TicketFlow API is a production-style FastAPI backend for support ticket management. It turns a reusable backend template into a focused portfolio project with authentication, role-based authorization, PostgreSQL persistence, Alembic migrations, and a tested ticket workflow.
- JWT authentication with active user checks
- User and admin roles
- Ticket creation, filtering, pagination, search, and statistics
- Admin assignment to active admins only
- Public and internal ticket comments
- Ticket history audit trail for creation, updates, comments, assignment, priority, and status changes
- Structured JSON error responses
- Health endpoints for app and database checks
- Docker Compose local stack
- Ruff, mypy, pytest, Alembic, and GitHub Actions
TicketFlow keeps HTTP handlers thin and places domain behavior in services.
app/api/routes: FastAPI routers and request dependency wiringapp/api/dependencies: auth and database dependenciesapp/models: SQLAlchemy 2.x ORM models and enumsapp/schemas: Pydantic v2 request and response schemasapp/repositories: database query, filtering, counting, and persistence helpersapp/services: authorization, workflow validation, timestamps, assignment rules, and history creationalembic/versions: database migrationstests: unit and integration coverage
- Python 3.12+
- FastAPI
- PostgreSQL
- SQLAlchemy 2.x
- Alembic
- Pydantic v2
- PyJWT and bcrypt password hashing
- Docker and Docker Compose
- Ruff, mypy, pytest
User: authenticated account withUSERorADMINrole and active statusTicket: support request with status, priority, category, creator, optional assignee, and lifecycle timestampsTicketComment: public or admin-only internal discussion on a ticketTicketHistory: immutable audit event for important ticket changes
Admin transitions:
OPEN -> IN_PROGRESSOPEN -> RESOLVEDIN_PROGRESS -> WAITING_FOR_CUSTOMERIN_PROGRESS -> RESOLVEDWAITING_FOR_CUSTOMER -> IN_PROGRESSWAITING_FOR_CUSTOMER -> RESOLVEDRESOLVED -> OPENRESOLVED -> CLOSEDCLOSED -> OPEN
Normal users may only reopen their own resolved ticket:
RESOLVED -> OPEN
Timestamp behavior:
- Moving to
RESOLVEDsetsresolved_at - Reopening from
RESOLVEDclearsresolved_at - Moving to
CLOSEDsetsclosed_at - Reopening from
CLOSEDclearsclosed_at
Normal users can:
- Create tickets
- List and view only their own tickets
- Update title and description only while a ticket is
OPEN - Add public comments to their own tickets
- Reopen their own
RESOLVEDticket toOPEN
Normal users cannot assign tickets, change priority/category, view history, view internal comments, close tickets, or access another user's ticket.
Admins can:
- List and view all tickets
- Update ticket details, priority, category, and status
- Assign tickets to active admins
- Add public or internal comments
- View ticket history
- Resolve, close, and reopen tickets
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
Copy-Item .env.example .envEdit .env for your local database and JWT secret, then run migrations:
python -m alembic upgrade head
python -m uvicorn app.main:app --reloadOpen:
- API docs:
http://127.0.0.1:8000/docs - Health:
http://127.0.0.1:8000/health - Database health:
http://127.0.0.1:8000/api/v1/health/database
docker compose up --buildValidate the compose file:
docker compose configCreate a migration after model changes:
python -m alembic revision --autogenerate -m "describe change"Apply migrations:
python -m alembic upgrade headInspect migration state:
python -m alembic heads
python -m alembic historypython -m ruff check .
python -m ruff format --check .
python -m mypy app
python -m pytest
python -m alembic heads
python -m alembic history
docker compose configAuthentication:
POST /api/v1/auth/registerPOST /api/v1/auth/loginGET /api/v1/auth/me
Users:
GET /api/v1/usersGET /api/v1/users/{user_id}PATCH /api/v1/users/{user_id}DELETE /api/v1/users/{user_id}
Tickets:
POST /api/v1/ticketsGET /api/v1/ticketsGET /api/v1/tickets/statisticsGET /api/v1/tickets/{ticket_id}PATCH /api/v1/tickets/{ticket_id}PATCH /api/v1/tickets/{ticket_id}/statusPATCH /api/v1/tickets/{ticket_id}/priorityPATCH /api/v1/tickets/{ticket_id}/categoryPATCH /api/v1/tickets/{ticket_id}/assigneePOST /api/v1/tickets/{ticket_id}/commentsGET /api/v1/tickets/{ticket_id}/commentsGET /api/v1/tickets/{ticket_id}/history
Health:
GET /healthGET /api/v1/health/database
List high-priority technical tickets:
Invoke-RestMethod `
-Uri "http://127.0.0.1:8000/api/v1/tickets?priority=HIGH&category=TECHNICAL" `
-Headers @{ Authorization = "Bearer $token" }Search title and description, newest first:
Invoke-RestMethod `
-Uri "http://127.0.0.1:8000/api/v1/tickets?q=billing&sort_by=created_at&sort_dir=desc" `
-Headers @{ Authorization = "Bearer $token" }Supported ticket list filters:
statusprioritycategoryassigned_to_idcreated_by_idis_assignedcreated_fromcreated_toqsort_by:created_at,updated_at,priority,statussort_dir:asc,desc
Normal users are always scoped to their own tickets, even when they pass broader filters.
Register:
Invoke-RestMethod `
-Method Post `
-Uri "http://127.0.0.1:8000/api/v1/auth/register" `
-ContentType "application/json" `
-Body '{"email":"customer@example.com","password":"StrongPassword123","full_name":"Customer User"}'Log in and store the bearer token:
$login = Invoke-RestMethod `
-Method Post `
-Uri "http://127.0.0.1:8000/api/v1/auth/login" `
-ContentType "application/json" `
-Body '{"email":"admin@example.com","password":"StrongPassword123"}'
$token = $login.access_tokenCreate a ticket:
Invoke-RestMethod `
-Method Post `
-Uri "http://127.0.0.1:8000/api/v1/tickets" `
-Headers @{ Authorization = "Bearer $token" } `
-ContentType "application/json" `
-Body '{"title":"Cannot export invoice","description":"The invoice export returns a blank PDF.","priority":"HIGH","category":"BILLING"}'Seed demo data:
python -m scripts.seedDemo credentials:
- Admin:
admin@example.com/StrongPassword123 - User:
user@example.com/StrongPassword123 - User:
jane@example.com/StrongPassword123
The seed creates assigned and unassigned tickets, public comments, internal comments, and history entries.
GitHub Actions is configured to run project quality checks for pull requests and pushes. The intended verification set is:
- Ruff lint
- Ruff format check
- mypy
- pytest
- Alembic metadata/history checks
- Passwords are hashed before storage.
- JWTs use a configurable secret from environment variables.
- Inactive users cannot authenticate.
- Normal users are always scoped to their own tickets at the service layer.
- Internal comments and ticket history are admin-only.
- Closed tickets are immutable except for explicit reopen transitions.
- Do not commit production secrets in
.env.
- SLA timers and breach reporting
- Email or webhook notifications
- Attachment storage for ticket evidence
- Saved views for support teams
- Full-text PostgreSQL search
- WebSocket updates for live ticket activity
erDiagram
USERS ||--o{ TICKETS : creates
USERS ||--o{ TICKETS : assigned
USERS ||--o{ TICKET_COMMENTS : writes
USERS ||--o{ TICKET_HISTORY : changes
TICKETS ||--o{ TICKET_COMMENTS : has
TICKETS ||--o{ TICKET_HISTORY : records
USERS {
uuid id
string email
string full_name
string role
boolean is_active
}
TICKETS {
uuid id
string title
text description
string status
string priority
string category
uuid created_by_id
uuid assigned_to_id
datetime resolved_at
datetime closed_at
}
TICKET_COMMENTS {
uuid id
uuid ticket_id
uuid author_id
text content
boolean is_internal
}
TICKET_HISTORY {
uuid id
uuid ticket_id
uuid changed_by_id
string event_type
text old_value
text new_value
}
flowchart TD
Client["Client"] --> Route["FastAPI route"]
Route --> Auth["Auth dependency"]
Auth --> Service["TicketService"]
Service --> Rules["Authorization and workflow rules"]
Rules --> Repo["TicketRepository"]
Repo --> DB[("PostgreSQL")]
Service --> History["TicketHistory event"]
History --> Repo
Service --> Response["Pydantic response"]
Response --> Client


