Automating document approval workflows with AI can dramatically improve efficiency, accuracy, and compliance—if done securely. In this practical tutorial, we'll guide you through building a secure, AI-powered document approval workflow from scratch, including user authentication, AI-based document classification, and multi-level approvals with audit trails.
As we covered in our complete guide to automating document approval workflows with AI, this area deserves a deeper look—especially when it comes to building secure, robust solutions. This tutorial provides a hands-on, code-first approach for developers and architects ready to build or upgrade their own approval systems.
Prerequisites
- Technical Skills: Intermediate Python (3.9+), basic Docker, REST APIs, basic understanding of OAuth2/JWT authentication.
- Tools & Versions:
- Python 3.9+
- Docker 24.x
- PostgreSQL 14+
- FastAPI 0.100+
- OpenAI API (or similar LLM provider)
- Git 2.30+
- Accounts: OpenAI API key (or alternative LLM API)
- Environment: Unix-like OS (Linux/macOS or WSL on Windows)
Overview
We'll build a secure workflow with these core components:
- User authentication (JWT-based)
- Secure document upload and storage
- AI-powered document classification (using OpenAI GPT-4 API)
- Multi-level approval logic
- Audit logging and access control
For a broader comparison of tools and platforms, see Top AI Tools for Document Approval Automation in 2026: A Hands-On Comparison.
Step 1: Project Setup and Secure Configuration
-
Clone the Starter Repository
git clone https://github.com/your-org/secure-ai-doc-approval.git
cd secure-ai-doc-approval
(If starting from scratch, create a new directory and initialize a Git repo.)
-
Create a Python Virtual Environment
python3 -m venv venv
source venv/bin/activate
-
Install Dependencies
pip install fastapi[all] sqlalchemy psycopg2-binary python-jose[cryptography] python-multipart openai
-
Set Up Environment Variables
Create a
.envfile:OPENAI_API_KEY=your-openai-key DATABASE_URL=postgresql://user:password@localhost:5432/approvaldb SECRET_KEY=your-very-secret-jwt-key ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=60Tip: Never commit secrets to git. Use
.gitignore! -
Initialize the Database (PostgreSQL)
docker run --name approvaldb -e POSTGRES_PASSWORD=password -e POSTGRES_USER=user -e POSTGRES_DB=approvaldb -p 5432:5432 -d postgres:14
Step 2: Implement Secure User Authentication
We'll use JWT tokens for secure, stateless authentication. FastAPI makes this straightforward.
-
Create User and Token Models (
models.py)from sqlalchemy import Column, Integer, String, Boolean from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True, index=True) hashed_password = Column(String) is_approver = Column(Boolean, default=False) is_admin = Column(Boolean, default=False) -
JWT Auth Utilities (
auth.py)from datetime import datetime, timedelta from jose import jwt def create_access_token(data: dict, secret: str, algorithm: str, expires_delta: timedelta): to_encode = data.copy() expire = datetime.utcnow() + expires_delta to_encode.update({"exp": expire}) return jwt.encode(to_encode, secret, algorithm=algorithm) -
Register/Login Endpoints (
main.py)from fastapi import FastAPI, Depends, HTTPException from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from sqlalchemy.orm import Session from auth import create_access_token import models app = FastAPI() oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") @app.post("/token") def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): user = authenticate_user(db, form_data.username, form_data.password) if not user: raise HTTPException(status_code=400, detail="Incorrect username or password") access_token = create_access_token( data={"sub": user.username}, secret=SECRET_KEY, algorithm=ALGORITHM, expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) ) return {"access_token": access_token, "token_type": "bearer"}Note: Implement
authenticate_userandget_dbas needed.
Step 3: Secure Document Upload and Storage
-
Create Document Model (
models.py)from sqlalchemy import ForeignKey, DateTime, Text from sqlalchemy.orm import relationship import datetime class Document(Base): __tablename__ = "documents" id = Column(Integer, primary_key=True, index=True) owner_id = Column(Integer, ForeignKey("users.id")) filename = Column(String) content = Column(Text) status = Column(String, default="pending") created_at = Column(DateTime, default=datetime.datetime.utcnow) owner = relationship("User") -
Document Upload Endpoint (
main.py)from fastapi import File, UploadFile, Depends @app.post("/documents/upload") def upload_document(file: UploadFile = File(...), current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): contents = file.file.read().decode("utf-8") doc = models.Document( owner_id=current_user.id, filename=file.filename, content=contents, status="pending" ) db.add(doc) db.commit() db.refresh(doc) return {"document_id": doc.id, "status": doc.status}Security Note: Always validate file types and scan for malware in production.
Step 4: Integrate AI-Powered Document Classification
-
Install OpenAI SDK
pip install openai
-
AI Classification Utility (
ai.py)import openai import os openai.api_key = os.getenv("OPENAI_API_KEY") def classify_document(text: str) -> str: prompt = f"Classify the following document for approval workflow (e.g., 'invoice', 'contract', 'policy', 'other'):\n\n{text[:1000]}" response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a document classification assistant."}, {"role": "user", "content": prompt} ], max_tokens=10 ) label = response['choices'][0]['message']['content'].strip().lower() return label -
Call AI Classifier After Upload (
main.py)from ai import classify_document @app.post("/documents/upload") def upload_document(...): ... doc_type = classify_document(contents) doc.status = "pending" doc.type = doc_type db.add(doc) db.commit() db.refresh(doc) return {"document_id": doc.id, "type": doc_type, "status": doc.status}
Step 5: Build the Approval Workflow Logic
-
Approval Model (
models.py)class Approval(Base): __tablename__ = "approvals" id = Column(Integer, primary_key=True) document_id = Column(Integer, ForeignKey("documents.id")) approver_id = Column(Integer, ForeignKey("users.id")) status = Column(String) # 'approved' or 'rejected' timestamp = Column(DateTime, default=datetime.datetime.utcnow) document = relationship("Document") approver = relationship("User") -
Approval Endpoint (
main.py)from fastapi import Body @app.post("/documents/{doc_id}/approve") def approve_document(doc_id: int, approved: bool = Body(...), current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): doc = db.query(models.Document).filter_by(id=doc_id).first() if not doc: raise HTTPException(status_code=404, detail="Document not found") if not current_user.is_approver: raise HTTPException(status_code=403, detail="Not authorized") approval = models.Approval( document_id=doc_id, approver_id=current_user.id, status="approved" if approved else "rejected" ) db.add(approval) doc.status = "approved" if approved else "rejected" db.commit() return {"document_id": doc.id, "status": doc.status}Tip: For multi-level approvals, add an
approval_levelfield and check previous approvals before updating status.
Step 6: Add Audit Logging and Access Controls
-
Audit Log Model (
models.py)class AuditLog(Base): __tablename__ = "audit_logs" id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id")) action = Column(String) document_id = Column(Integer, ForeignKey("documents.id")) timestamp = Column(DateTime, default=datetime.datetime.utcnow) -
Log Actions in Endpoints
def log_action(db: Session, user_id: int, action: str, document_id: int): log = models.AuditLog(user_id=user_id, action=action, document_id=document_id) db.add(log) db.commit() log_action(db, current_user.id, "approved" if approved else "rejected", doc_id) -
Restrict Access Based on Roles
def get_current_approver_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): user = get_current_user(token, db) if not user.is_approver: raise HTTPException(status_code=403, detail="Approver access required") return user
Step 7: Secure Deployment (Docker + HTTPS)
-
Dockerize the App (
Dockerfile)FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] -
Run with Docker Compose (
docker-compose.yml)version: "3.9" services: db: image: postgres:14 restart: always environment: POSTGRES_USER: user POSTGRES_PASSWORD: password POSTGRES_DB: approvaldb ports: - 5432:5432 app: build: . ports: - "8000:8000" depends_on: - db env_file: - .env -
Enable HTTPS (Reverse Proxy Example with Caddy)
:443 reverse_proxy app:8000 tls you@yourdomain.comFor production, always use HTTPS. Consider using a managed certificate provider or a reverse proxy like Caddy or Nginx.
Common Issues & Troubleshooting
- OpenAI API errors: Check your API key, rate limits, and ensure your environment variable is set.
-
JWT token errors: Ensure your
SECRET_KEYmatches across all services and is not empty. -
Database connection issues: Confirm your Docker container is running and
DATABASE_URLis correct. -
File upload fails: Check FastAPI's
python-multipartis installed and that file size limits are not exceeded. - Access denied on endpoints: Verify user roles and JWT token scopes.
Next Steps
You now have a working, secure AI-powered document approval workflow! To further enhance your solution:
- Implement advanced approval routing and escalation policies.
- Integrate with external DMS or cloud storage for document archiving.
- Add real-time notifications (email, Slack, etc.) for approvals and rejections.
- Perform regular security audits—see Securing Real-Time AI Workflows: Essential Strategies for 2026 for guidance.
- Review Best Practices for Automating Document Approval Workflows with AI in 2026 to ensure your implementation is robust and compliant.
For more platform options, benchmarks, and security guidance, revisit our 2026 Guide to Automating Document Approval Workflows With AI.
Screenshots (Descriptions)
- Figure 1: Terminal output showing successful Docker Compose up and FastAPI server running on port 8000.
- Figure 2: Swagger UI (http://localhost:8000/docs) displaying available endpoints for document upload and approval.
- Figure 3: Example JSON response for a successful document upload, showing document_id, type, and status.