Skip to content

All posts

Platform

Where your content goes, and the one condition on zero egress

We follow one document from the API call to a search result and name every place its text exists. A security reviewer can then check the zero-egress promise, and its one condition, instead of taking it on faith.

Glowing blue fibre-optic strands against a dark background

Every retrieval platform says it keeps your data safe. A security reviewer needs something more useful: a list of every place the text of a document exists, how long it stays there, and which connections it crosses. This post gives that list for foundation4.

We follow one document from the moment an application submits it to the moment its text appears in a search result. At each step we name the component, what it holds, and for how long. At the end, the zero-egress promise and its one condition follow from the map rather than from a slogan.

The short version

foundation4 runs entirely inside your environment. No foundation4 cloud service takes part in any request, and foundation4 sends no telemetry to the vendor. Not a single byte of your content leaves your perimeter, on one condition: content leaves only if an administrator registers a model server outside the perimeter.

The rest of this post shows why that is the condition, and the only one.

Step 1: the API call

An application submits a document as plain text in a JSON body. This is the example from the ingestion guide:

curl -X POST "$FOUNDATION4_URL/pipelines/$PIPELINE_ID/documents" \
  -H "x-api-key: $FOUNDATION4_API_KEY" \
  -H "x-api-key-secret: $FOUNDATION4_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "classification": "public",
    "external_identifier": "kb-account-lockout",
    "metadata": {"product": "accounts"},
    "contents": "Account lockout. After five failed sign-in attempts, an account is locked for 15 minutes. An administrator can unlock the account from the Users page."
  }'

The request reaches the API server, usually through an ingress that terminates TLS. Every request carries an API key secret, so every client path to foundation4 should use TLS.

The API server validates the classification and the metadata. It records the document in PostgreSQL with the status pending, stores the raw text in the NATS JetStream object store, and places a processing job on the queue. It then answers at once:

{
  "id": "01a0f18a-ca80-754e-ba2b-46e126f7cf9e",
  "external_identifier": "kb-account-lockout",
  "classification": "public",
  "status": "pending",
  "version": 1790758800722235
}

At this point the text exists in two places: in transit to the API server, and in the NATS object store.

Step 2: the queue holds raw text, briefly

The NATS object store is the only place foundation4 keeps the submitted text in its original form. It holds that text until a worker has processed the document, and for at most 24 hours.

When a worker finishes, it deletes the raw text from the object store. Text that no worker processes, such as a document that keeps failing or one deleted while still pending, stays until the 24-hour limit removes it.

Why it matters: the queue is a second store of plain text, even if a short-lived one. The docs ask operators to restrict NATS JetStream to the foundation4 pods with network policies, and to encrypt its volumes at the storage level. A review should check both.

Step 3: the worker splits, embeds and encrypts

A worker picks up the job. For each document it:

  1. splits the text into fragments with the pipeline's text splitter;
  2. computes a vector for each fragment, when the pipeline has an embedding model;
  3. builds full-text index entries, when full-text search is enabled;
  4. encrypts the fragment text;
  5. writes the fragments, vectors and index entries to PostgreSQL, then sets the status to success.

Two of these steps can move text to another process.

Splitting. Text splitters run in the gRPC service, a sidecar container in the same Kubernetes pod as the worker. The worker reaches it on the pod's loopback interface. The text does not leave the pod for this step.

Embedding. Where the embedding runs depends on the provider. FastEmbed models run inside the worker process. GPT4All and Hugging Face Sentence Transformers models run in the gRPC sidecar. An OpenAI-compatible embedding endpoint, or a Hugging Face inference endpoint, is a separate server. Requests to it carry the fragment text. That server is a model server, and its location decides whether this step stays inside your network.

Encryption. Stored text is encrypted with a separate key for each classification. The worker uses ChaCha20-Poly1305 with the key of the document's classification and a new random nonce for each fragment.

Step 4: PostgreSQL, the one place content persists

After processing, the only stored copy of the text is the set of encrypted fragments in PostgreSQL. foundation4 cannot hand back the submitted document word for word. Your source system remains the authoritative copy.

PostgreSQL holds more than ciphertext, and a reviewer should know exactly what:

  • Encrypted fragment text, one key per classification.
  • The classification keys themselves, in the same database. A full database backup therefore contains both the ciphertext and the keys.
  • Vectors, unencrypted, because nearest-neighbor search compares them inside the database. Vectors are computed from the fragment text.
  • Full-text index entries, unencrypted. These are the normalized words of each fragment with their positions.
  • Metadata, unencrypted, because filters compare it inside the database.

The connection between foundation4 and PostgreSQL carries the classification keys with each read of fragment text, and the fragment text of pipelines with full-text search. That is why the database guide recommends sslmode=verify-full in production.

Step 5: the search path

Now a reader searches. The application sends the query, the classifications the reader holds and a metadata filter:

{
  "query": "How do customers get their money back?",
  "type": "similarity",
  "classification": ["internal"],
  "params": {"k": 5},
  "filters": {"product": {"$eq": "billing"}}
}

The query text goes to the same places the document text went. For vector search, the API server embeds the query with the pipeline's embedding model, in process, in the sidecar or on an embedding server. For full-text search, the query text goes to PostgreSQL.

PostgreSQL returns the matching fragments with the key of each fragment's classification, read in the same query. The API server decrypts the text and returns the results to the application. Results are scoped to each reader by the classifications and metadata your application supplies.

Decrypted text exists in the API server's memory and in the response. It is not written anywhere new.

Step 6: the cache, and what it does not hold

foundation4 uses a Redis-compatible cache (Valkey in the default installation). It holds short-lived data only:

  • API key verification results, encrypted with the application secret, for 15 minutes.
  • Frequently read objects, such as pipeline settings, for minutes.
  • Traces of generation requests, only when a request turns tracing on. A trace holds the messages sent to the model and the retrieved fragments with their decrypted text. It is encrypted with the application secret and kept for 60 minutes.

Search results are not on that list. The cache holds no data needed for recovery and is rebuilt automatically.

Where the text lives, and for how long

This summarizes the architecture and data protection pages:

What Where How long
Raw document text waiting to be processed NATS JetStream object store Until a worker processes it, 24 hours at most
Fragment text (encrypted), vectors, full-text entries, metadata PostgreSQL, one set of tables per pipeline Until the document is deleted
Pipelines, documents, versions, models, API keys, permissions PostgreSQL, shared tables Until deleted
Classification keys PostgreSQL Until the classification is removed or the pipeline is deleted
API key verification results (encrypted) Cache 15 minutes
Frequently read objects Cache Minutes
Generation traces, when tracing is on (encrypted) Cache 60 minutes
Embedding model files and Python packages Container images mounted into pods For the life of the release

The persistent state of a deployment is short: the PostgreSQL database, the application secret, and the identifier and secret of the master key. NATS holds only work in progress. Back up those three things, and treat the database backup with the same care as the database.

Deletion, versions and backups

Expiring a document removes it from search but keeps its version history, including the encrypted text, readable for point-in-time reads. Only deletion removes the text. Deleting a document removes every version, with its fragments, vectors and full-text entries.

Deletion does not reach backups. Content deleted after a backup was taken stays in that backup, and in archived write-ahead log files, until your backup retention removes them. Backup retention is therefore part of any erasure plan.

What model servers see

foundation4 includes no language model and has no default model server. It contacts a model server only when a feature uses a registered one. Any OpenAI-compatible model works, hosted or inside your network.

Two kinds of model server can receive your content:

  • Embedding servers receive fragment text during ingestion and query text during search, when a pipeline uses a remote embedding endpoint.
  • Language model servers receive prompts when generation runs on a registered model server. A prompt that includes retrieved fragments carries their decrypted text.

The API server is the only component that calls a language model, so all outbound generation traffic comes from one place. That keeps network policy simple: the egress rule can name only the model server addresses. A request to a model server uses TLS when its endpoint URL starts with https://.

This is why the zero-egress promise has a condition. foundation4 sends content to the model servers an administrator registers, because that is how embedding and generation work. If every registered server is inside your network, content stays inside. If an administrator registers a server outside, content goes there.

Built for isolated networks

foundation4 is built for isolated networks. The qualifier matters: some embedding models and the token text splitter download files on first use. The models and packages page lists what works offline:

  • FastEmbed models shipped in the API server image, and custom ONNX models in a model image.
  • OpenAI-compatible embedding servers inside the network.
  • The recursive character, character, code and Markdown header splitters.
  • Generation, with a model server inside the network.

The other eight built-in FastEmbed models download from Hugging Face on first use. The token splitter downloads its encoding files. The GPT4All and Sentence Transformers libraries can contact their services while loading a model, so the docs advise testing them before relying on them in an isolated network.

These downloads fetch files. They do not send your content. With internal endpoints and offline-capable models and splitters, foundation4 makes no connection outside your network.

A checklist for your review

  • Every registered LLM and embedding endpoint resolves to a server inside your perimeter.
  • Pipelines use only models and splitters that work offline.
  • Network policies admit only foundation4 pods to PostgreSQL, NATS and the cache. Internal traffic is unencrypted by default.
  • PostgreSQL uses sslmode=verify-full, and NATS volumes and the database have storage-level encryption.
  • Database roles are limited, because the keys sit beside the ciphertext.
  • The application secret and master key are stored apart from the backup files.
  • The API server and workers log at warn, the chart default. Lower levels add lines for database statements, so keep them for short diagnosis only.
  • Backup retention matches your deletion obligations.

The security at a glance page and the security hardening guide cover each point in detail. To walk through this map against your own environment, request a briefing.

See foundation4 inside your perimeter.

Tell us about your environment and we will walk you through how foundation4 fits.

Request a briefing