SynapCores

Build a Local RAG App With Citations in One Database

Published on October 29, 2026

Watch: Same RAG App, Two Stacks: 4 Services vs 1 Database

We built the same RAG app twice, with the same documents, the same models and the same five questions, and we counted everything. The first version is the stack most tutorials use: Postgres with pgvector, Ollama for embeddings and chat, an ingest job and a query API, all in Docker Compose. The second version is one database, SynapCores, and one Python file.

This post walks you through the second version from a fresh Mac to cited answers. Every command and every answer below comes from a real run on SynapCores v2.4.0 Community Edition, which is free.

What you'll build

You'll build a support bot for a made-up router company, Orbit Networks. It answers questions from four short policy documents, cites the file each answer came from, and says it doesn't know when the documents don't cover something. When you drop a new document into the folder, the bot can answer questions about it a few seconds later, with no re-indexing step.

Here is how the two versions compared on the same Mac, on the same day:

The usual stack SynapCores
Services to run 4 (Postgres + pgvector, Ollama, ingest job, query API) 1 database process
Container images 3 pulled + 1 built 0 (one native binary)
Files you write 8 1
Lines of glue code (Python + SQL) 92 28
Lines of config 57 0 (you change two model names in the installed config)
Code + config total 149 28
Cold setup to first answer, including model downloads 5:34 3:35
Correct answers 5/5 5/5
Right source cited 5/5 5/5
New file in the docs folder re-run the ingest job picked up automatically in about 3 seconds

Both stacks used qwen2.5:7b for chat and nomic-embed-text for embeddings, with temperature 0, seed 7 and the top 3 chunks. Lines are counted without blank lines, comments or docstrings.

Prerequisites

  • An Apple silicon Mac. We used an M-series MacBook with 24 GB of memory.
  • Homebrew. The installer uses it for three media libraries (ffmpeg, tesseract and leptonica).
  • Python 3.9 or newer (python3 --version).
  • About 6 GB of free disk space and a few minutes of download time for the two models.

Step 1: Install SynapCores

Open Terminal and run the official installer. SYNAPCORES_USER_INSTALL=1 installs everything for your user only, so you don't need sudo:

curl -fsSL https://get.synapcores.com | SYNAPCORES_USER_INSTALL=1 sh

The installer asks three questions:

  • Install the Homebrew dependencies? Answer Y.
  • Pre-pull the default LLM? Answer n. You'll pick the models for this tutorial in the next step, and SynapCores downloads them on first start.
  • Enable semantic verification (NLI)? Press Enter to skip it. You don't need it for this tutorial.

When it finishes you have:

~/.local/bin/synapcores          the database (one binary)
~/.local/bin/synapcores-start    a small start helper
~/.synapcores/gateway.toml       the config file
~/.synapcores/synapcores.env     private secrets the installer generated for you

Step 2: Choose the models

Open ~/.synapcores/gateway.toml in any text editor, find the [query.ai_service] section and set the two model lines:

[query.ai_service]
provider        = "local"
model           = "qwen2.5:7b"
embedding_model = "nomic-embed-text"
auto_pull       = true

Or make the same change from the terminal:

sed -i '' \
  -e 's|^# model omitted.*|model           = "qwen2.5:7b"|' \
  -e 's|^embedding_model = .*|embedding_model = "nomic-embed-text"|' \
  ~/.synapcores/gateway.toml

These are the same two models the usual stack runs in Ollama, so the comparison is like for like. SynapCores runs them in-process, on the Mac's GPU, and pulls them from the same Ollama registry. You don't install Ollama.

Step 3: Start the database

Start SynapCores in the foreground. Leave this terminal window open while you work through the tutorial:

~/.local/bin/synapcores-start \
  --env-file ~/.synapcores/synapcores.env \
  --binary ~/.local/bin/synapcores \
  --config ~/.synapcores/gateway.toml

On the first start it prints a FIRST-BOOT CREDENTIALS box. Copy the admin password from it, because you'll need it in a moment:

===============================================================
  FIRST-BOOT CREDENTIALS
===============================================================
  ADMIN LOGIN
  username: admin
  password: ••••••••••••••••

Then it downloads both models:

  ┃ downloading model 'nomic-embed-text' …
  ┃ downloading model 'qwen2.5:7b' …

    SynapCores HTTP server is ready.
    AI model: qwen2.5:7b — downloading

Open a second terminal window and check whether the models are ready. While they download you'll see "state":"downloading". Run it again until it says "ready":true. That takes a few minutes, depending on your connection:

curl -s localhost:8080/health
{"status":"ok", ... "ai":{"model":null,"state":"ready","ready":true, ...}}

Step 4: Create an API key

Every request to SynapCores carries an API key. In the second terminal, paste your admin password between the quotes, then run the block. It logs in as admin and asks for a key that can read and write, valid for 30 days:

ADMIN_PW='paste-the-admin-password-here'

TOKEN=$(curl -s -X POST localhost:8080/v1/auth/login \
  -H 'content-type: application/json' \
  -d "{\"username\":\"admin\",\"password\":\"$ADMIN_PW\"}" \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')

curl -s -X POST localhost:8080/v1/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"name":"rag-demo","permission":"FullAccess","expires_in_days":30}' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["raw_key"])'

It prints your new key, which starts with aidb_. Save it somewhere safe, because it's shown only once. Then put it in an environment variable for this terminal:

export SC_KEY='aidb_...'

Step 5: Get the sample docs

Create a project folder, a Python virtual environment, and the one library the app uses (httpx):

mkdir -p ~/rag-demo/docs && cd ~/rag-demo
python3 -m venv .venv
source .venv/bin/activate
pip install httpx

Download the four sample documents:

B=https://synapcores.com/demos/two-stacks
for f in returns-and-refunds shipping troubleshooting-error-codes warranty; do
  curl -fsSL -o docs/$f.md $B/docs/$f.md
done

They're short. Here they are in full, so you can see what the bot knows.

docs/returns-and-refunds.md

# Orbit Networks: Returns and Refunds Policy (rev. 2026-09)

Hardware purchased directly from Orbit Networks can be returned within **45 days of delivery** for a full refund.
Opened software licenses (Orbit Shield, Orbit Mesh Manager) are not refundable.
Refurbished units are returnable within 14 days only.
Restocking fee: none for unopened items; 10% for opened items without the original box.
Refunds are issued to the original payment method within 5 business days of the item arriving at our Reno warehouse.

docs/shipping.md

# Orbit Networks: Shipping

Orders over $120 ship free within the contiguous US (3 to 5 business days).
Alaska and Hawaii: 6 to 9 business days, flat $15.
Express shipping (2 business days): $24.
We do not ship to PO boxes for routers due to carrier signature requirements.
International shipping is not available as of September 2026.

docs/troubleshooting-error-codes.md

# Orbit X2 / X2 Pro: Status light and error codes

- **E-417**: WAN negotiation failed. Unplug the modem for 60 seconds, then power on the modem first and the router second.
- **E-418**: DHCP lease not received. Set WAN mode to "Dynamic IP" in the app under Internet > Connection Type.
- **E-512**: Firmware signature mismatch. Do NOT power off. Wait 10 minutes; the router rolls back automatically. If the light stays red, hold the reset button for 30 seconds.
- **E-700**: Mesh backhaul too weak. Move the M1 node within 10 meters of the router or use a wired backhaul.
- Blinking amber light: firmware update in progress (normal, up to 8 minutes).
- Solid red light: hardware fault. File a warranty claim.

docs/warranty.md

# Orbit Networks: Limited Warranty

| Product | Warranty |
|---|---|
| Orbit X2 router | 3 years |
| Orbit X2 Pro router | 5 years |
| Orbit Mesh node M1 | 2 years |
| Orbit Power Adapter PA-30 | 1 year |

Not covered: water damage, lightning/power surges without an Orbit surge protector, and units with a removed serial sticker.
To file a claim, open a ticket with your serial number (format ORB-XXXX-XXXX) at support.orbit.example.

Step 6: The app, rag_app.py

Download the app:

curl -fsSLO https://synapcores.com/demos/two-stacks/rag_app.py

This is the whole thing:

"""The same rag app on SynapCores: point it at the folder once, then ask with `rag`."""
import json
import os
import sys

import httpx

db = httpx.Client(base_url=os.environ.get("SC_URL", "http://127.0.0.1:8080"),
                  headers={"Authorization": "Bearer " + os.environ["SC_KEY"]}, timeout=600)


def index(folder):
    r = db.post("/v1/filesystem-collections", json={
        "name": "orbit_support", "filesystem_path": folder, "file_types": ["md"], "watch": True,
    })
    r.raise_for_status()
    return r.json()["data"]


def ask(question):
    r = db.post("/api/chat", json={
        "model": "qwen2.5:7b", "stream": False,
        "messages": [{"role": "user", "content": question}],
        "options": {"temperature": 0, "seed": 7},
        "rag": {"source": "filesystem:orbit_support", "top_k": 3, "strict": True},
    })
    r.raise_for_status()
    return r.json()["message"]["content"], json.loads(r.headers.get("x-synapcores-citations", "[]"))


if __name__ == "__main__":
    if sys.argv[1] == "index":
        c = index(os.path.abspath(sys.argv[2]))
        print("watching", c["filesystem_path"], "| embeddings:", c["embedding_model"])
    else:
        answer, sources = ask(sys.argv[2])
        print(answer, "\nsources:", sources)

Here's what each block does.

The client. db is a plain HTTP client pointed at SynapCores on port 8080. Every request sends your API key as a bearer token. The timeout is generous because the first answer after a start loads the model.

index(folder). This registers the folder as a filesystem collection, named orbit_support, for Markdown files. SynapCores reads each file, splits it into chunks, embeds the chunks with nomic-embed-text and keeps the vectors right next to the text. "watch": True tells it to keep watching the folder, so a new file gets indexed without you doing anything. In the usual stack, this one call is ingest.py, schema.sql and a Postgres container.

ask(question). This is SynapCores' Ollama-compatible /api/chat endpoint, with the same body you'd send to Ollama plus one extra field, rag. That field tells the database to retrieve the top 3 chunks from filesystem:orbit_support, put them in the prompt, and (because of strict) answer only from them. The answer comes back in message.content, the same as with Ollama. The sources come back in the x-synapcores-citations response header. In the usual stack, this one call is api.py, rag.py, a FastAPI container and an Ollama container.

The command line. python rag_app.py index <folder> indexes a folder, and python rag_app.py ask "<question>" asks a question.

Step 7: Index the docs and ask

Index the folder:

python rag_app.py index docs
watching /Users/you/rag-demo/docs | embeddings: nomic-embed-text

SynapCores indexes the four files in the background, which takes about a second. Then ask the five test questions. Four of them are answered by the docs. The fifth is about price matching, which the docs don't cover, so the right answer is "I don't know".

python rag_app.py ask "My Orbit X2 router shows error E-512. What should I do? Should I unplug it?"
For error code **E-512** on your Orbit X2 router, which indicates a firmware signature mismatch, you should not power off the device immediately. Instead, follow these steps:

1. Wait for 10 minutes to see if the issue resolves itself as the router attempts an automatic rollback.
2. If the light remains red after 10 minutes, hold down the reset button on the router for 30 seconds. This can help trigger a firmware reset.

If the problem persists after these steps, it might be advisable to contact Orbit Networks support with your serial number and describe the issue in detail. They may provide further guidance or request that you file a warranty claim if necessary.
sources: ['filesystem:orbit_support/troubleshooting-error-codes.md#0', 'filesystem:orbit_support/warranty.md#0', 'filesystem:orbit_support/returns-and-refunds.md#0']
python rag_app.py ask "How many years of warranty does the Orbit X2 Pro router have?"
The Orbit X2 Pro router comes with a 5-year warranty.
sources: ['filesystem:orbit_support/warranty.md#0', 'filesystem:orbit_support/troubleshooting-error-codes.md#0', 'filesystem:orbit_support/returns-and-refunds.md#0']
python rag_app.py ask "How long do I have to return a refurbished unit?"
You have 14 days from the delivery date to return a refurbished unit if you wish to do so according to Orbit Networks' returns and refunds policy.
sources: ['filesystem:orbit_support/returns-and-refunds.md#0', 'filesystem:orbit_support/warranty.md#0', 'filesystem:orbit_support/troubleshooting-error-codes.md#0']
python rag_app.py ask "How much does shipping to Hawaii cost, and how long does it take?"
Shipping to Hawaii from Orbit Networks costs $15 with a delivery time of 6 to 9 business days.
sources: ['filesystem:orbit_support/shipping.md#0', 'filesystem:orbit_support/returns-and-refunds.md#0', 'filesystem:orbit_support/warranty.md#0']
python rag_app.py ask "Do you offer price matching?"
The provided context does not contain any information about price matching policies. Therefore, based on the given information alone, I cannot confirm whether Orbit Networks offers a price matching policy or not.
sources: ['filesystem:orbit_support/shipping.md#0', 'filesystem:orbit_support/returns-and-refunds.md#0', 'filesystem:orbit_support/warranty.md#0']

That's 5 out of 5. Each answer is correct, and the first source listed is the file that holds the answer: error codes, warranty, returns, shipping. For price matching, the bot says the docs don't cover it instead of inventing a policy. With temperature 0 and a fixed seed you should see the same answers. We got them word for word on three separate runs.

The list of sources is the top 3 chunks that were retrieved, ranked best first. Each document here is short enough to be a single chunk, which is why every source ends in #0.

Step 8: Add a file and ask about it

The folder is watched, so you can add a document and ask about it straight away. Create a fifth doc:

cat > docs/orbit-care-plus.md <<'EOF'
# Orbit Care+

Orbit Care+ extended protection costs $49 per year and adds accidental damage cover.
EOF

Wait about three seconds, then ask:

python rag_app.py ask "How much does Orbit Care+ cost?"
Orbit Care+ costs $49 per year and adds accidental damage coverage.
sources: ['filesystem:orbit_support/orbit-care-plus.md#0', 'filesystem:orbit_support/shipping.md#0', 'filesystem:orbit_support/warranty.md#0']

You didn't re-run anything. SynapCores noticed the new file, chunked and embedded it, and the very next question cited it. In our run the collection went from 4 files to 5 in 2.7 seconds. With the usual stack, this is the point where you run the ingest job again.

The usual stack, for comparison

The other half of the video is the same app built the way most RAG tutorials build it. You can download it, along with everything from this post, in one zip: two-stacks-demo.zip.

It's a clean, typical setup that runs well:

  • db: pgvector/pgvector:pg17, with a chunks table and an HNSW cosine index (schema.sql).
  • ollama: ollama/ollama, serving both the embedding model and the chat model.
  • ingest: a one-shot job (ingest.py) that chunks each file with overlap, embeds the chunks with nomic's document prefix, and replaces that file's rows in a transaction.
  • api: a FastAPI service (api.py) that embeds the question, runs the vector search, builds a "use only this context" prompt, calls the chat model and returns the sources.

Shared code lives in rag.py, and the container is described by Dockerfile and requirements.txt. With docker-compose.yml and .env, that's 8 files and 149 lines. It also answered 5 out of 5 with the right sources. The difference is everything you build and run to get there: start the services, pull the two models into Ollama, run the ingest job, then ask. The zip's README.md has the exact commands.

Next steps

Get SynapCores

SynapCores Community Edition is free and self-hosted, with no signup. Download SynapCores and build the app above in a few minutes. Then point it at your own docs.