About the project — Surf AI (synap.surf)
Tagline: Surf your files locally — answers stay on your device.
Demo: https://synap.surf
Inspiration
We kept hitting the same trade-off: AI chat is useful for résumés, bank CSVs, and private folders — but almost every tool wants those files in someone else’s cloud. Local stacks (Ollama, RAG, vector DBs) solve privacy, yet they feel like a weekend of Terminal work for a non-engineer.
Surf AI started from a simple bet: the website should be a shell, not a vault. Deliver a calm chat UI from the browser; keep chats, attachments, and model inference on the device. If someone wants a stronger offline path, give them a one-line zip setup on a real computer — not a fake “local” product that still ships prompts to a frontier API.
The YC × Moss Local-First AI / Small Cloud track pushed us further: treat local-first as a product constraint. Moss is the default semantic search when credentials are present; keyword search is only the automatic fallback when keys fail or the browser is fully offline. If we could not point to where the bytes live (IndexedDB, Ollama, Moss session on disk), we did not claim the feature.
What it does
Surf AI is local-first document chat:
- Attach PDFs, Word/Excel, CSV, slides, code, or project zips (no image upload — we cannot see pixels).
- Ask questions in the browser; answers are grounded in extracted file text, not a cloud LLM that keeps your files.
- Run an in-browser light model (WebLLM) and/or Ollama on the machine; chats stay in IndexedDB.
- Use Moss SDK first for on-device semantic search (no traditional vector DB, no
push_indexto Moss Cloud); fall back to keyword search when needed. - Download zip for offline desktop use — same agent, zip unpack, purpose-first explainers, and anti-loop repair.
- Stay honest about limits: short answers on 1B models, conversion not supported, phones use browser chat only (no Terminal setup).
How we built it
synap.surf (static Next.js export)
│
▼
Browser agent (React)
├─ IndexedDB — threads, files, saved summary
├─ WebLLM — in-tab light model (≈ 1B params)
├─ groundedContext — purpose-first explainers + anti-loop repair
└─ optional HTTPS → FastAPI host
├─ Ollama (1b / 3b)
└─ Moss SDK first → keyword fallback
Download zip → Python serves local-agent.html on 127.0.0.1
(same explainers + loop repair + zip unpack)
| Layer | Choice |
|---|---|
| UI | Next.js App Router, output: 'export' |
| Storage | Browser IndexedDB |
| In-browser model | WebLLM (Llama 3.2–class 1B) |
| Desktop model | Ollama (llama3.2:1b / 3b, …) |
| Host API | FastAPI — auth, feedback, /v1/chat stream |
| Search | Moss SDK primary; keyword fallback |
| Documents | pdf.js + OOXML + CSV / code / zip unpack |
| Prompting | CRISPE system.md (anti-repetition) + grounded overrides |
| Offline pack | LOCAL-SETUP + local-agent.html (parity with web) |
| Quality | Harness: Moss flow, reply repair, résumé≠YAML |
Build path: static shell → extract text before the model → type-aware grounding (explainOneFile) → WebLLM and/or host Ollama → Moss-first retrieval when keys exist → light-model rails (extractiveFactAnswer, repairLoopedReply) → deploy static out/ + host on the VM.
Challenges we ran into
Grounding that looked like hallucination. Summaries of zips/PDFs became meta essays about the “document agent,” or a résumé was labeled “YAML config” because of lines like Languages:. We added overview detection, type-aware explainers, extractive facts/topics, resume≠YAML guards, and stopped using file-overview as the default for unrelated questions.
Light models loop. Receipts and lists repeated the same bullet ten times. We added anti-repetition rules in system.md and a fixed pipeline (repairLoopedReply: soft-dedupe → extractive rescue → hard-cap) on both the website and the downloaded agent, with harness tests.
Offline zip stub. The downloaded agent answered every zip ask with “folders exist to separate code…” even when Ollama was local. We built rich explainZipProject from the real tree and stopped forcing extractive when local Ollama is up and only the WAN is down.
Moss skipped in practice. Chat always sent offline: true, which forced keyword fallback. We made Moss SDK the default when online + keys are present, keyword when offline/creds fail, and locked that in harness moss-flow-* cases.
Small demo hardware. A tight VM cannot run a large model snappily. We lean on llama3.2:1b / in-browser 1B, label short answers, and point fuller quality at the desktop zip + larger Ollama tags.
Scope discipline. Tempting to claim vision, conversion, or cloud LLMs. We shipped only what we can demo: attach → extract → ask → answer on-device, with Moss for on-device search when configured.
Accomplishments that we're proud of
- A working local-first product on synap.surf: attach real files, get grounded answers, delete data in this browser.
- Moss-first retrieval wired end-to-end (SDK primary, keyword secondary, never
push_index), with harness coverage. - Purpose-first explainers across file types (CSV, code, zip, receipts, résumés) so “what is this?” teaches meaning instead of dumping text.
- Light-model product rails that stop loops and answer short facts (e.g. tenure at a company) extractively when the 1B model would invent or mislabel.
- Offline zip parity — judges can download the pack and get the same zip summary + loop repair without Next.js.
- An honest privacy story: the site is a shell; chats and indexes stay on the device.
What we learned
- Local-first is a data-plane choice. Name the store (IndexedDB, host SQLite, Ollama weights, Moss session) — don’t sell vibes.
- Moss must be default, not decoration. One bad flag (
offline: truealways) can hide the whole integration. - Extraction + type detection beats clever prompting.
Languages:is not YAML;Park+must not match insidePySpark. - Small models need product rails. Guidelines plus fixed repair iterations beat hoping 1B will stop looping.
- Offline zip must ship the same rails. Stale packs and stub answers are demo-killers.
- Honesty scales. Clear limits (no vision, no conversion, phone vs desktop) build more trust than fake features.
What's next for synap.surf
- Stronger desktop-first experience: clearer Moss key setup, one-click local host attach, and better 3B+ default when the machine can take it.
- Richer spreadsheet Q&A (column totals, filters) and deeper codebase walkthroughs on unpacked zips.
- Optional citations UI (highlight the exact span used) without becoming a full notebook clone.
- Keep improving light-model reliability (more fact patterns, fewer false type labels) and keep the downloaded agent in lockstep with the site.
- Stay true to Small Cloud: more capability on the user’s machine, still zero chat vault on the static host.
Built With
- fastapi
- indexeddb
- livekit
- llama
- moss
- next.js
- nginx
- ollama
- pdf.js
- python
- react
- sqlite
- typescript
- webgpu
- webllm
Log in or sign up for Devpost to join the conversation.