Skip to main content
Olatunde Adedeji
  • Home
  • Expertise
  • Case Studies
  • Books
  • Blog
  • Contact

Site footer

AI Applications Architect & Full-Stack Engineering Leader

Designing and delivering production-grade AI systems, grounded retrieval workflows, and cloud-native platforms for teams building real products.

Explore

  • Home
  • Expertise
  • Case Studies

Resources

  • Books
  • Blog
  • Contact

Let's Collaborate

Available for architecture advisory, AI product collaboration, technical writing, and selected consulting engagements.

  • LinkedIn
  • GitHub
  • X (Twitter)
← Blog
SecurityApril 1, 2026·10 min read·By Olatunde Adedeji

Securing EviVault: Authentication, Access Control, and Predictable Boundaries

A deep dive on how EviVault uses JWT authentication, password hashing, user scoping, and clear access boundaries to make a grounded AI product secure and predictable.

A good AI product still has to be a good product.

That sounds simple, but it is one of the easiest things to underplay in AI application work. Teams often spend most of their energy on retrieval, prompts, embeddings, and model output quality, then treat authentication and access control as supporting concerns to handle later. In practice, those “supporting concerns” are part of the trust layer too.

EviVault Assistant was built as an internal document intelligence system. Users upload policies, guides, procedures, and other knowledge assets, then query them through a grounded retrieval workflow. That makes security more than a technical checkbox. The system is not just generating text. It is handling documents that may be sensitive, operationally important, or user-specific.

That means the product needs clear boundaries.

It needs to know who the user is, what documents they should be allowed to access, and how to fail predictably when those boundaries are crossed. It also needs to do this in a way that keeps the architecture understandable rather than hiding everything inside brittle shortcuts.

That is why EviVault uses a relatively simple but disciplined security model built around authentication, per-user scoping, and predictable access behavior.

The role of security in a grounded AI product

It is tempting to think of security as separate from trust. In a system like EviVault, the two are closely connected.

A product can show evidence, attach confidence labels, and abstain on weak support, but it will still lose credibility if it leaks another user’s document, exposes a resource that should be hidden, or handles access inconsistently.

A grounded assistant should not only answer with evidence. It should retrieve from the right evidence.

That shifts security from a background implementation detail into a product requirement.

For EviVault, the security layer needed to do a few things well:

  • authenticate users cleanly
  • store passwords safely
  • scope document access per user
  • protect routes consistently
  • avoid leaking information through error behavior
  • keep the system maintainable as the product grows

Those goals shaped the overall design.

Why JWT fit this project

EviVault uses JWT-based authentication.

That choice made sense because the application is API-driven, built on FastAPI, and structured around a frontend that sends authenticated requests to protected backend routes. JWT provides a stateless mechanism for identifying the user across requests without requiring a server-side session store for basic authentication.

The basic flow is straightforward:

text
Register or log in → receive signed token → store token on client → send token with each protected request

That pattern fit the architecture well. It keeps the backend simple, works naturally with SPA-style frontends, and makes route protection easy to express through FastAPI dependencies.

The point was not to build a complicated auth system. The point was to build one that was clear, practical, and sufficient for the product’s boundaries.

Password hashing comes first

Before the system can authenticate anyone, it has to store credentials safely.

EviVault uses bcrypt through passlib for password hashing:

python
from passlib.context import CryptContext pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def hash_password(password: str) -> str: return pwd_context.hash(password) def verify_password(plain: str, hashed: str) -> bool: return pwd_context.verify(plain, hashed)

This matters because password handling is one of the easiest places to make the whole system fragile. Plaintext passwords are unacceptable. Reversible encryption is the wrong model. Fast hashing is also the wrong choice because it makes brute-force attacks easier.

bcrypt is useful here because it is intentionally slow and includes salting behavior that improves resistance to common attack patterns. That makes it a strong default for a product like this.

The bigger point is that even in an AI application, the ordinary rules of good software still apply. Security fundamentals do not become optional just because the interface happens to include retrieval and language generation.

Creating and signing the access token

Once a user is authenticated successfully, the backend creates a signed access token.

A representative implementation looks like this:

python
from datetime import datetime, timedelta, timezone from jose import jwt def create_access_token(data: dict, expires_delta: timedelta = None) -> str: to_encode = data.copy() expire = datetime.now(timezone.utc) + ( expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) ) to_encode.update({"exp": expire}) return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)

This function does a few important things.

It copies the token payload, adds an expiry time, and signs the result using the application secret key and configured algorithm. In EviVault, the token’s sub claim carries the user identifier, which allows the system to map the token back to the correct user later.

This is not unusual or flashy, and that is exactly the point. Good authentication systems are often boring in the right ways. Predictability is valuable.

A representative configuration looks like this:

python
SECRET_KEY: str = "change-me-in-production" ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24

The placeholder value for SECRET_KEY exists to force explicit production configuration. In a real deployment, this key should be replaced with a strong random value rather than something readable or reused.

Protecting routes with dependency injection

One of the cleanest parts of the FastAPI-based security model is that route protection can stay declarative.

EviVault uses an auth dependency to decode the JWT, validate it, and retrieve the corresponding user:

python
from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") def get_current_user( token: str = Depends(oauth2_scheme), db: Session = Depends(get_db), ) -> User: credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) user_id: str = payload.get("sub") if user_id is None: raise credentials_exception except JWTError: raise credentials_exception user = db.query(User).filter(User.id == user_id).first() if user is None: raise credentials_exception return user

This function is important because it keeps security logic out of the route body. The route does not need to manually inspect headers, decode tokens, or verify the user on every protected action. It simply depends on get_current_user.

That leads to cleaner endpoints:

python
@router.post("/ask", response_model=AskResponse) def ask_question( payload: AskRequest, db: Session = Depends(get_db), user: User = Depends(get_current_user), ): ...

By the time the route runs, the user has already been authenticated. That makes the request flow easier to read and easier to maintain.

Authentication is only part of the story

Knowing who the user is is not enough.

In a document intelligence platform, the system also needs to know what that user is allowed to see. This is where access control becomes more important than simple authentication.

EviVault treats documents as user-owned resources. Each document carries an owner_id, and retrieval or management operations must respect that relationship.

A representative model field looks like this:

python
class Document(Base): owner_id: Mapped[str] = mapped_column( String(36), ForeignKey("users.id"), nullable=False )

This means that document access is not inferred indirectly. It is encoded in the data model itself.

That choice makes later security checks clearer and more reliable.

Per-user scoping at retrieval time

This is one of the most important security decisions in the whole system.

The vector store handles semantic search, but it does not inherently understand product-level ownership rules. That means the application layer needs to enforce access boundaries when retrieval results come back.

A representative ownership check looks like this:

python
doc = db.query(Document).filter( Document.id == doc_id, Document.owner_id == user_id, Document.status == "ready", ).first() if doc is None: continue

This matters for two reasons.

First, it prevents the system from surfacing chunks from documents the user does not own or should not access.

Second, it keeps the separation of concerns clean. ChromaDB handles nearest-neighbor search. The relational layer handles ownership and lifecycle semantics. The application logic ties the two together.

This is a strong architectural pattern because it keeps security explicit instead of pretending the vector layer can solve every product concern on its own.

Why returning 404 can be the right choice

Security is not only about who can access a resource. It is also about what the system reveals when access should be denied.

EviVault uses a deliberate pattern when users try to access documents they should not see:

python
doc = db.query(Document).filter( Document.id == doc_id, Document.owner_id == user.id, ).first() if not doc: raise HTTPException(status_code=404, detail="Document not found")

Returning 404 instead of 403 in this context is a thoughtful choice.

A 403 Forbidden response confirms that the resource exists but access is denied. A 404 Not Found response reveals less. It does not help an unauthorized user learn whether the document exists at all.

That is a small detail, but it reflects the larger security posture of the platform: do not leak information unnecessarily.

Document listing and deletion follow the same rules

A secure product should behave consistently across operations. The same ownership boundaries used in retrieval should apply when users list, inspect, or delete documents.

For example, a document listing route can scope results directly:

python
docs = db.query(Document).filter( Document.owner_id == user.id ).order_by(Document.created_at.desc()).all()

Deletion follows the same principle:

python
doc = db.query(Document).filter( Document.id == doc_id, Document.owner_id == user.id, ).first() if not doc: raise HTTPException(status_code=404, detail="Document not found")

This consistency matters. Predictable access behavior helps users understand the system, and it helps developers maintain it without scattering slightly different security rules across the codebase.

Role-aware access for administration

EviVault also includes a simple role distinction between regular users and admins.

A representative user model field looks like this:

python
role: Mapped[str] = mapped_column(String(50), default="user")

This allows admin-only actions to be gated cleanly:

python
if user.role != "admin": raise HTTPException(status_code=403, detail="Insufficient permissions")

This is a lightweight RBAC model, and that is appropriate for the project.

The goal was not to design a large enterprise IAM system. It was to create enough structural clarity to separate normal user activity from platform management activity.

Simple role boundaries often age better than over-engineered permission systems introduced too early.

Why predictable boundaries matter in AI systems

This is where the title theme matters most.

A product like EviVault does not just need authentication. It needs predictable boundaries.

Users should know that:

  • their documents are scoped to them
  • unauthorized resources stay hidden
  • protected endpoints behave consistently
  • the system will not cross ownership lines just because the retrieval layer found a semantically relevant chunk

Those expectations are part of product trust.

In a grounded AI assistant, the answer experience and the access model should reinforce each other. The product should feel careful about truth and careful about boundaries.

That combination is more reassuring than a system that talks confidently about evidence while behaving loosely with access control.

What this part of the project taught me

Building the security layer reinforced a few practical lessons.

First, authentication is necessary but not sufficient. Real product trust depends heavily on access control and ownership enforcement.

Second, security logic should be clear and composable. Dependency injection, scoped queries, and explicit ownership checks make the system easier to reason about.

Third, ordinary software discipline matters just as much in AI products as in any other product. Password hashing, token expiry, role checks, and careful error behavior are not side details.

Fourth, vector search does not remove the need for application-level authorization. Semantic relevance is not permission.

Final Thoughts

EviVault’s security model is intentionally straightforward.

It hashes passwords with bcrypt, authenticates users with signed JWTs, protects routes through FastAPI dependencies, scopes documents per user, and keeps resource visibility predictable through consistent access checks and careful error responses.

That may not be the flashiest part of the system, but it is one of the most important.

A grounded AI assistant should not only answer with evidence.

It should operate within boundaries users can trust.

SecurityAuthenticationAccess ControlJWTFastAPIAI EngineeringInternal Tools
Share
XLinkedInFacebook