Paperless‑ngx‑style Document Manager (Python+SQLite)

A 45‑minute beginner clone of Paperless‑ngx

Welcome to another official UVF IT guide! In this quick build you’ll create a tiny document management system inspired by Paperless‑ngx, using only Python, FastAPI, and a local SQLite database—no paid services required.

We’ll scaffold a FastAPI app, define a simple Document model, add upload & list endpoints, run it locally, and verify you can store and view PDFs—all in under an hour.

Before you start

Minimal Architecture
LOCAL STACK SQLModel save file FastAPI App Python + SQLite SQLite DB File Storage ./uploads
1

Phase 1: Scaffold the project

Create a virtual environment so our dependencies stay isolated from the system Python. Then install FastAPI for the web framework, Uvicorn as the ASGI server, and SQLModel which gives us an easy ORM on top of SQLite. Finally we add a minimal main.py that starts the app – this file will grow as we add routes later.

setup.sh

#!/usr/bin/env bash
# 1. Create and activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate
# 2. Upgrade pip and install core packages
pip install --upgrade pip
pip install fastapi uvicorn sqlmodel
# 3. Create project skeleton
mkdir -p app/uploads
cat > app/main.py <<'PY'
from fastapi import FastAPI
app = FastAPI()

@app.get('/')
async def root():
    return {'msg': 'Paperless‑ngx clone ready'}
PY
echo "Setup complete. Run with: uvicorn app.main:app --reload"
2

Phase 2: Define the Document model (data)

A Document needs an identifier, the original filename, and the moment it was uploaded. SQLModel lets us declare this as a Python class; when the app starts it will automatically create the corresponding SQLite table if it doesn't exist yet.

app/models.py

from datetime import datetime
from sqlmodel import Field, SQLModel

class Document(SQLModel, table=True):
    id: int = Field(default=None, primary_key=True)
    filename: str = Field(index=True)
    uploaded_at: datetime = Field(default_factory=datetime.utcnow)

# Helper to create the DB tables – call from main later
def init_db(engine):
    SQLModel.metadata.create_all(engine)
3

Phase 3: Build upload & list API (core feature)

In this step we add the heart of our clone: a FastAPI endpoint that accepts multipart file uploads. The uploaded file is stored in a local ./uploads folder and a record is inserted into the SQLite database so we can retrieve it later. We also expose a simple GET endpoint that returns a JSON list of all stored documents, showing their IDs and filenames. This mirrors Paperless‑ngx’s basic document ingestion and browsing capabilities.

main.py

import os
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
import sqlite3

UPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)

app = FastAPI(title="Paperless‑ngx Clone")

DB_PATH = "documents.db"

def get_db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn

@app.on_event("startup")
def init_db():
    with get_db() as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS documents (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                filename TEXT NOT NULL,
                path TEXT NOT NULL
            )
        """)
        conn.commit()

class Document(BaseModel):
    id: int
    filename: str
    path: str

@app.post("/upload", response_model=Document)
async def upload_file(file: UploadFile = File(...)):
    file_location = os.path.join(UPLOAD_DIR, file.filename)
    with open(file_location, "wb") as buffer:
        content = await file.read()
        buffer.write(content)
    with get_db() as conn:
        cur = conn.execute("INSERT INTO documents (filename, path) VALUES (?, ?)", (file.filename, file_location))
        doc_id = cur.lastrowid
        conn.commit()
    return Document(id=doc_id, filename=file.filename, path=file_location)

@app.get("/documents", response_model=list[Document])
def list_documents():
    with get_db() as conn:
        rows = conn.execute("SELECT id, filename, path FROM documents").fetchall()
        return [Document(id=row["id"], filename=row["filename"], path=row["path"]) for row in rows]
4

Phase 4: Run locally

Now that the API is defined, we launch the development server so you can interact with it. Uvicorn will serve the FastAPI app and automatically reload on code changes, which is handy while you experiment. Once running, open your browser to the /docs path to see the interactive Swagger UI that FastAPI generates for you.

uvicorn main:app --reload
5

Phase 5: Verify & extend

With the server up, use the Swagger UI (http://127.0.0.1:8000/docs) to upload a PDF file and then call the GET /documents endpoint to see it listed. You can also perform the same actions from the command line with curl to automate testing. Feel free to explore the ./uploads folder – the raw file you sent should be there, confirming the end‑to‑end flow works.

curl -X POST "http://127.0.0.1:8000/upload" -F "file=@sample.pdf"

Verifying the Stack

CheckCommand / Why it matters
Server starts without errorsuvicorn main:app --reload shows 'Uvicorn running on http://127.0.0.1:8000'
Upload works via Swagger UI or curlPOST /upload returns JSON with id and filename
GET /documents returns a JSON array with your uploaded fileVisit http://127.0.0.1:8000/documents in browser

What’s next?

You now have a functional clone. Add OCR with Tesseract, implement tags, or secure the API with JWT to approach the full Paperless‑ngx feature set.