Skip to content

Project management with a permission-aware AI assistant

TaskTracker

Internship at ABA Technology (Nextronic) · Jul–Aug 2026 · solo build, nine sprints

In short

TaskTracker is the project and task management platform I built for ABA Technology during my internship — requirements, design, code and deployment, on my own, over nine agile sprints.

It covers the daily work of a delivery team: projects and clients, tasks on a Kanban board, timesheets, real-time messaging, statistics. And an assistant that answers questions like "which of my tasks are overdue?" in plain language, from the organisation's own data. The part worth reading about is how that assistant is kept from answering with data the person asking shouldn't see.

The problem

The brief was a management tool; the constraints came from the stakeholders. 19 functional requirements, gathered and formalised with them before any code, and a permission model with five roles — administrator, project manager, team lead, collaborator and read-only observer — each seeing a different slice of the same organisation.

That second point is what made the AI part hard. A retrieval-augmented assistant searches whatever it has indexed. If it has indexed the whole organisation, asking it a question is a way around every access rule the rest of the application enforces: the assistant becomes the one place where an observer can read what a manager wrote. Adding an assistant was easy. Adding one that doesn't leak was the actual requirement.

Architecture

A layered Next.js 16 application: server actions call services, services own the business rules, Prisma owns the database. Nothing in the browser is trusted for authorisation.

  • Sessions are signed with HMAC-SHA256 and verified with a constant-time comparison. Every action re-checks role and organisation on the server; the organisation boundary is applied in each query, not inferred from the URL.
  • PostgreSQL with pgvector, eleven tables. Projects, tasks and project notes are embedded with nomic-embed-text and stored as knowledge chunks next to the data they came from — re-indexed whenever that data is created, edited or deleted.
  • The assistant runs on Ollama (llama3.2:3b by default), so the organisation's data never goes to a third-party API. It answers only from the retrieved context, and every answer comes back with its sources.
  • Real-time messaging runs on a standalone WebSocket server that authenticates connections with the same signed session cookie as the app; the app pushes events to it through a secret-protected endpoint.
  • Docker Compose runs all four services — app, database, model server, WebSocket server — and seeds a reproducible demo organisation on first start.

Decisions & trade-offs

The permission filter runs inside the vector search

The usual shape of RAG is: search the index, then filter the results. That fails twice. It leaks — rows you can't see still decided which five chunks came back. And it starves — if the closest matches are all out of scope, you get an empty answer even though relevant, visible data exists.

So the scope comes first: the project, task and note ids this user may read. It's applied in the SQL WHERE clause, before ORDER BY embedding <=> query, so the nearest neighbours are chosen among permitted rows only. The cost is one scope query per question; the benefit is that there is no code path where the model sees a row the user couldn't open.

Grounded answers with sources, from a small local model

A 3-billion-parameter model will happily invent a deadline. The prompt restricts it to the retrieved context and tells it to say so when the context doesn't hold the answer, and the response carries the ids of the chunks it was built from, so any claim can be traced to the task it came from. In a team tool, a small model that says "I don't know" is worth more than a large one that guesses — and a local one keeps the data in the building.

One session for two servers

The WebSocket server is its own process, because a serverless deployment can't hold connections open. The tempting shortcut is a second, simpler token for the socket. Instead it reads the same HMAC-signed cookie the app issues: one authentication scheme to get right, not two to keep in step. And the only way to broadcast to clients is through the app, behind a shared secret.

The online demo drops features rather than faking them

Vercel can't run a WebSocket server or a local model. Rather than simulate either, the online version is explicit that live refresh and the assistant need the Docker setup, and the README gives an evaluator three ways in: online, full Docker, or local development. A demo that pretends is worse than one that is honest about its limits.

What was hard

Running a model on a CPU. Inside Docker an answer takes between 30 seconds and two minutes, the first one slowest while the model loads. That drove the practical choices: a generous, configurable timeout; a small default model that an environment variable can swap; and error messages that name which of the likely causes you've hit — model not downloaded, index empty, machine too slow, assistant disabled — instead of a generic failure.

Keeping the index honest. An embedding is a copy of the data, and copies go stale. Every write that changes a project, task or note updates or removes its chunk in the same service call, and a reindex script rebuilds everything from scratch when needed. A stale index doesn't crash; it just answers, confidently, with last week's deadline.

Outcome

A complete application, containerised: the sprint plan run to completion, five roles, real-time messaging and a working assistant, shipped as four services that start with one command and seed the same demo data on every machine. It runs online for browsing and in Docker for the full evaluation.

What I took from it: owning a service end to end means the unglamorous parts are yours too — the seed data, the troubleshooting table, the timeout message — and those are the parts an evaluator meets first.

What I'd do next

Move the login rate limiter out of process memory. It counts failed attempts per instance, which is fine on one container and wrong behind a load balancer; Postgres or Redis would fix it.

Then measure the assistant instead of trusting it: a small set of questions with known answers, run on every change to the prompt or the model, so retrieval quality is a number rather than an impression.