Ask questions about UFO documents.
A portable, production-ready RAG (Retrieval-Augmented Generation) web app. Upload UFO PDFs and images, and chat with them β answers come back grounded in your own files, with citations to the exact source passages.
Built with Next.js 14 (App Router) Β· TypeScript Β· Tailwind CSS Β· Prisma Β· PostgreSQL Β· local embeddings (Transformers.js).
- Document upload β drag-and-drop for PDFs and images.
- Bulk ingestion CLI β pull in official government UAP releases (ODNI, AARO,
CIA FOIAβ¦) from a curated manifest, a folder, or a URL (
npm run ingest). - Text extraction β PDF parsing (
pdf-parse) + OCR for images (tesseract.js). - Local embeddings β semantic vectors generated on-device with Transformers.js
(
all-MiniLM-L6-v2). No external embedding API required, so the app is fully portable. - Semantic search β cosine-similarity retrieval over stored vectors. Works on any standard PostgreSQL (no special extension needed).
- Cited answers β an LLM composes the answer and cites the passages it used (
[1],[2]β¦). - Document library β search, filter, reprocess, and delete documents with live status.
- Settings / health β built-in system status page (DB, LLM, embeddings, env validation).
- Cosmic UI β responsive, modern, space-themed interface with a UFO logo.
Upload ββΆ Local file storage (uploads/)
β
βΌ
Extract text (PDF parse / image OCR)
β
βΌ
Chunk ββΆ Embed locally (Transformers.js) ββΆ Store vectors (Postgres)
β
Question ββΆ Embed query ββΆ Cosine similarity ββΆ Top-k chunks β
β
βΌ
LLM answer + citations
| Layer | Choice |
|---|---|
| Framework | Next.js 14 (App Router), React 18, TypeScript |
| Styling | Tailwind CSS + shadcn/ui |
| Database | PostgreSQL via Prisma ORM |
| Storage | Local filesystem (uploads/, configurable via UPLOADS_DIR) |
| Embeddings | Transformers.js (Xenova/all-MiniLM-L6-v2) β local |
| Extraction | pdf-parse (PDF) Β· tesseract.js (image OCR) |
| LLM | Claude API (Anthropic SDK) β claude-opus-4-8 |
- Node.js 18+ (20+ recommended) and Yarn (or npm)
- A PostgreSQL database (local, Docker, or hosted)
- (Optional) An Anthropic API key for LLM answer generation. Without it the app returns extractive answers (the most relevant passages).
The embedding model (~90 MB) is downloaded automatically on first use and cached.
# 1. Install dependencies
yarn install # or: npm install
# 2. Configure environment
cp .env.example .env
# then edit .env with your DATABASE_URL and (optionally) ANTHROPIC_API_KEY
# 3. Set up the database schema
yarn prisma generate
yarn prisma db push # creates tables from prisma/schema.prisma
# 4. Run the dev server
yarn dev # http://localhost:3000yarn build
yarn start # serves the production build on port 3000Uforia ships with a bulk ingestion CLI and a curated manifest of official U.S.
government UAP/UFO releases (data/sources.json) β
including the ODNI Preliminary Assessment (2021), the ODNI annual UAP reports,
and the AARO Historical Record Report. Ingested documents go through the same
pipeline as UI uploads: store β extract text β chunk β embed.
# Ingest everything in data/sources.json
npm run ingest
# Verify the remote sources are reachable first (no DB writes)
npm run ingest -- --check
# Ingest a folder of PDFs/images you downloaded yourself
npm run ingest -- --dir ~/Downloads/uap-docs
# Ingest a single document by URL
npm run ingest -- --url https://example.gov/report.pdf --name AARO-Report.pdf
# Crawl a web page (and its sub-pages) for PDFs/images and ingest what it finds
npm run ingest -- --page https://www.archives.gov/research/topics/uaps --match "uap|ufo"A data/sources.json entry can point at a web page instead of a document β
the ingester scans the page and its sub-pages (same site only) for PDF/image
links and ingests them:
{
"type": "page",
"title": "National Archives β UAP topic page (crawled)",
"url": "https://www.archives.gov/research/topics/uaps",
"depth": 1,
"match": "uap|ufo|pbb|blue.?book|unidentified"
}depthβ how many levels of sub-pages to follow (default 1)limitβ optional max documents per source (default: unlimited)matchβ case-insensitive regex; only follows links matching it (keeps the crawl on-topic and skips site logos/icons)
The crawler is capped at 30 page fetches per source with a politeness delay.
npm run ingest -- --check dry-runs the crawl and lists what it would ingest
without touching the database.
The crawler extracts href/src links and scans linked .csv/.json data
files for document URLs β many JS-rendered government pages (e.g. the DoD's
https://www.war.gov/UFO/ interactive, whose 1,500+ record index lives in a
uap-data.csv) expose their whole document list that way.
βΉοΈ Beyond that, the crawler reads static HTML only. Pages that render their document lists purely with JavaScript (e.g. catalog.archives.gov search results) won't expose links to it β download those in a browser and use
--dir.
β οΈ Government sites often block non-browser clients (dni.gov and media.defense.gov sit behind bot protection). Ifnpm run ingestreports download failures, open the URLs fromdata/sources.jsonin a browser, save the PDFs into a folder, and runnpm run ingest -- --dir <folder>.
Where to find new releases to add to data/sources.json (or download for --dir):
| Source | URL |
|---|---|
| AARO (DoD All-domain Anomaly Resolution Office) reports | https://www.aaro.mil/ |
| National Archives UAP Records Collection | https://www.archives.gov/research/topics/uaps |
| CIA FOIA Reading Room β UFO collection | https://www.cia.gov/readingroom/collection/ufos |
| FBI Vault β UFO files | https://vault.fbi.gov/UFO |
Ingestion is idempotent β documents already in the database (matched by file
name) are skipped before they are downloaded, so re-running npm run ingest
only pulls documents that are new since the last run. Documents that errored
during processing are retried on the next run.
π₯ Note: only PDFs and images are supported. Video releases (e.g. the Navy "Tic Tac" footage) can't be ingested; official transcripts or reports about them can.
Uforia uses Prisma with three models (see prisma/schema.prisma):
- Document β uploaded file metadata + processing status + extracted text.
- DocumentChunk β text chunks and their embedding vectors (
Float[]). - ChatMessage β chat history with stored source citations.
Create the schema against your database:
yarn prisma db push # quick sync (great for getting started)
# or, for tracked migrations:
yarn prisma migrate dev --name initRetrieval computes cosine similarity in application code, which keeps Uforia
portable to any Postgres and is plenty fast for thousands of chunks. For very large
corpora you can install the pgvector
extension and switch DocumentChunk.embedding to a vector column with an
in-database <=> query. The app code is structured so this is an isolated change
in lib/retrieval.ts.
See .env.example for the full annotated list. Summary:
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
β | PostgreSQL connection string |
UPLOADS_DIR |
β | Where uploaded files are stored (default: uploads/ in the project root) |
ANTHROPIC_API_KEY |
β | Enables Claude answer generation |
CHAT_MODEL |
β | Chat model (default claude-opus-4-8) |
EMBEDDING_MODEL |
β | Local embedding model (default Xenova/all-MiniLM-L6-v2) |
NEXT_PUBLIC_SITE_URL |
β | Public base URL for social images |
The Settings page (/settings) runs a live health check and tells you exactly
which variables are missing.
The included docker-compose.yml spins up the app plus PostgreSQL so you can
run the whole stack locally with one command:
docker compose up --buildThen open the app at http://localhost:3000. Uploaded files persist on the
uploads_data volume; the database on db_data.
To build just the app image:
docker build -t uforia .
docker run -p 3000:3000 --env-file .env uforia- Push this project to a Git repository and import it into Vercel.
- Add the environment variables from
.env.examplein the Vercel dashboard. - Provision a Postgres database (e.g. Vercel Postgres, Neon, Supabase) and set
DATABASE_URLaccordingly. Note that file storage is on the local disk, so a host with a persistent filesystem is required (serverless filesystems are ephemeral β see the note below). - Set the build command to
prisma generate && next build.
Note: image OCR and local embeddings load native/WASM models. For consistently low latency and to avoid serverless cold-start limits, a long-running Node host (Docker, Render, Railway, Fly.io, a VM) is recommended over pure serverless.
yarn install
yarn prisma generate && yarn prisma db push
yarn build
yarn start.
βββ app/
β βββ api/
β β βββ chat/route.ts # RAG: retrieve + answer
β β βββ documents/route.ts # list documents
β β βββ documents/upload/route.ts # upload + kick off processing
β β βββ documents/[id]/route.ts # get status / delete
β β βββ documents/[id]/reprocess/... # retry processing
β β βββ health/route.ts # config/health check
β βββ upload/page.tsx # drag-and-drop upload
β βββ documents/page.tsx # library / management
β βββ chat/page.tsx # chat interface
β βββ settings/page.tsx # settings & status
β βββ layout.tsx # shell, navbar, footer, starfield
β βββ page.tsx # landing page
βββ components/ # UI components (logo, navbar, chat, etc.)
βββ hooks/use-documents.ts # documents data + polling
βββ lib/
β βββ db.ts # Prisma client
β βββ env.ts # env validation
β βββ storage.ts # local file storage helpers
β βββ pdf.ts # PDF text extraction
β βββ ocr.ts # image OCR
β βββ embeddings.ts # local embeddings + chunking + cosine similarity
β βββ retrieval.ts # semantic search
β βββ llm.ts # answer generation + extractive fallback
β βββ process.ts # full ingestion pipeline
βββ prisma/schema.prisma # Document, DocumentChunk, ChatMessage
βββ public/ # logo, favicon, OG image
βββ Dockerfile
βββ docker-compose.yml
βββ .env.example
- PDF (
application/pdf) β text extracted directly. - Images (
image/png,image/jpeg,image/webp,image/gif,image/bmp,image/tiff) β text extracted via OCR.
Scanned/image-only PDFs may yield little text. Upload the pages as images so OCR can run.
MIT β use it, fork it, explore the unknown. πΈ