Build a Local RAG App With Citations in One Database (28 Lines of Python) A developer built the same retrieval-augmented generation (RAG) support bot twice—once on a conventional stack of Postgres with pgvector, Ollama, an ingest job and a query API, and once on the single-database SynapCores v2.4.0 Community Edition—and measured both on the same Mac. The SynapCores version required 28 lines of code and config versus 149 for the usual stack, ran as one native binary with no containers, and answered all five test questions correctly with the right source cited in both cases, while picking up new documents in about three seconds without a re-indexing step. Both versions used qwen2.5:7b for chat and nomic-embed-text for embeddings at temperature 0, seed 7 and top 3 chunks. 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. 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. python3 --version . 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: Y . n . You'll pick the models for this tutorial in the next step, and SynapCores downloads them on first start. 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 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. 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, ...}} 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 ...' Create a project folder, a Python virtual environment, and the one library the app uses httpx https://www.python-httpx.org/ : 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. 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