A containerized note-taking application that provides authenticated CRUD operations for user-owned notes.
The project is designed to be easy to run locally with Docker Compose and includes a Flask API, PostgreSQL database, Vite frontend, automated tests, and CI quality/security checks.
The API allows authenticated users to:
- create notes
- retrieve all of their notes
- retrieve an individual note
- update note content or title
- delete notes
Each note is owned by a specific user, and all note queries are scoped to the authenticated user's JWT identity.
Users cannot read, modify, or delete notes owned by another user.
The current implementation includes authentication and token refresh support, but does not yet expose a public user registration endpoint.
You only need:
- Docker
- Docker Compose
Create the backend environment file:
cp backend/.env.example backend/.envCreate the frontend environment file:
cp frontend/.env.example frontend/.envThe backend environment contains values such as:
FLASK_DEBUG=false
DATABASE_URL=postgresql+psycopg://app:password@database:5432/app
JWT_SECRET_KEY=generate-a-secure-random-valueUse a secure random value for JWT_SECRET_KEY.
PostgreSQL configuration is stored in:
config/database.env
From the repository root:
docker compose up --buildDocker Compose starts:
| Service | Address |
|---|---|
| Vite frontend | http://localhost:5173 |
| Flask API | http://localhost:5000 |
| PostgreSQL | localhost:5432 |
The application runs entirely within containers, so a local Python, PostgreSQL, or Node installation is not required for normal application startup.
To stop the environment:
docker compose downTo also remove the PostgreSQL data volume:
docker compose down -vThe backend uses Flask-Migrate/Alembic.
Apply existing migrations after startup with:
docker compose exec backend uv run flask --app api.api db upgradeTo create a new migration:
docker compose exec backend uv run flask --app api.api db migrate -m "describe migration"Then apply it:
docker compose exec backend uv run flask --app api.api db upgrade- Docker
- Docker Compose
- Python
- Flask
- Flask-SQLAlchemy
- SQLAlchemy 2.x
- PostgreSQL
- Flask-JWT-Extended
- Flask-Migrate / Alembic
- uv
- Vite
- React
- Bootstrap
Vite is used as the frontend development/build environment, with React providing the UI component layer.
- pytest
- Ruff
- mypy
- Bandit
- pip-audit
All note routes require an authenticated JWT access token.
Requests to protected routes should include:
Authorization: Bearer <access_token>The examples below assume the backend is available at:
http://localhost:5000
POST /loginRequest:
{
"email": "user@example.com",
"password": "password"
}Successful response:
{
"access_token": "<jwt-access-token>",
"refresh_token": "<jwt-refresh-token>",
"role": "user"
}Response:
200 OK
Invalid credentials return:
401 Unauthorized
POST /refreshSend the refresh token:
Authorization: Bearer <refresh_token>Response:
{
"access_token": "<new-access-token>"
}POST /auth/meRequires an access token.
Example response:
{
"isValid": true,
"user_id": "1",
"role": "user"
}A note is represented as:
{
"id": 1,
"title": "Project ideas",
"content": "Build a note-taking API.",
"created_at": "2026-09-06T20:00:00+00:00",
"updated_at": "2026-09-06T20:00:00+00:00"
}| Field | Type | Description |
|---|---|---|
id |
integer | Unique note identifier |
title |
string | Required title, maximum 255 characters |
content |
string | Note body |
created_at |
datetime | Time the note was created |
updated_at |
datetime | Time the note was last modified |
GET /notes/Returns all notes owned by the authenticated user.
Example:
curl http://localhost:5000/notes/ \
-H "Authorization: Bearer $ACCESS_TOKEN"Response:
[
{
"id": 1,
"title": "Project ideas",
"content": "Build a note-taking API.",
"created_at": "2026-09-06T20:00:00+00:00",
"updated_at": "2026-09-06T20:00:00+00:00"
}
]Response:
200 OK
GET /notes/<note_id>Example:
curl http://localhost:5000/notes/1 \
-H "Authorization: Bearer $ACCESS_TOKEN"Successful response:
200 OK
If the note does not exist or does not belong to the authenticated user:
404 Not Found
{
"error": "Note not found"
}POST /notes/Request:
{
"title": "Project ideas",
"content": "Build a note-taking API."
}Example:
curl -X POST http://localhost:5000/notes/ \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Project ideas",
"content": "Build a note-taking API."
}'Successful response:
201 Created
{
"id": 1,
"title": "Project ideas",
"content": "Build a note-taking API.",
"created_at": "2026-09-06T20:00:00+00:00",
"updated_at": "2026-09-06T20:00:00+00:00"
}A title is required and must not exceed 255 characters.
Invalid input returns:
400 Bad Request
PATCH /notes/<note_id>The endpoint supports partial updates.
Update only the title:
{
"title": "Updated title"
}Update only the content:
{
"content": "Updated note content."
}Update both:
{
"title": "Updated title",
"content": "Updated note content."
}Example:
curl -X PATCH http://localhost:5000/notes/1 \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Updated title"
}'Successful response:
200 OK
Invalid input returns:
400 Bad Request
A note that does not exist or does not belong to the authenticated user returns:
404 Not Found
DELETE /notes/<note_id>Example:
curl -X DELETE http://localhost:5000/notes/1 \
-H "Authorization: Bearer $ACCESS_TOKEN"Successful response:
204 No Content
A note that does not exist or does not belong to the authenticated user returns:
404 Not Found
| Method | Endpoint | Authentication | Description |
|---|---|---|---|
POST |
/login |
No | Authenticate a user |
POST |
/refresh |
Refresh JWT | Generate a new access token |
POST |
/auth/me |
Access JWT | Retrieve authenticated user information |
GET |
/notes/ |
Access JWT | Retrieve all notes owned by the user |
GET |
/notes/<id> |
Access JWT | Retrieve one note |
POST |
/notes/ |
Access JWT | Create a note |
PATCH |
/notes/<id> |
Access JWT | Partially update a note |
DELETE |
/notes/<id> |
Access JWT | Delete a note |
The user ID is never accepted from the client when accessing notes.
Instead, ownership is derived from the authenticated JWT:
user_id = int(get_jwt_identity())Database queries are scoped to both the requested note and the authenticated user:
db.select(Note).where(
Note.id == note_id,
Note.user_id == user_id,
)This prevents users from accessing notes belonging to another account.
Requests for a note belonging to another user return:
404 Not Found
rather than exposing whether the resource exists.
This avoids leaking information about resources owned by other users.
Updates use:
PATCHrather than PUT.
This allows clients to update the title or content independently without resending the entire note.
Note content is stored as application data rather than being transformed by the backend for presentation.
Formatting and rendering decisions can therefore remain with the frontend.
The API uses JWT-based authentication through Flask-JWT-Extended.
Two token types are issued:
- access tokens for authenticated API requests
- refresh tokens for obtaining new access tokens
The JWT identity contains the authenticated user's database ID.
Authorization decisions are made server-side using that identity rather than trusting user identifiers supplied by clients.
User passwords are stored as password hashes rather than plaintext values.
Authentication and authorization are handled separately:
- JWT validation determines who the user is
- database ownership checks determine which resources that user may access
All note CRUD routes enforce both.
Known dependency vulnerabilities are checked using:
uv run pip-auditPython source code is scanned using Bandit:
uv run bandit -r srcThe backend test suite uses pytest.
From the backend directory:
uv run pytestThe test suite covers:
- authentication
- note creation
- note retrieval
- partial updates
- deletion
- model behavior
- input validation
- JWT-protected endpoints
- cross-user authorization
Authorization tests specifically verify that one user cannot:
- read another user's note
- modify another user's note
- delete another user's note
GitHub Actions runs automated checks against the backend.
uv run pytestuv run ruff check .uv run ruff format --check .uv run mypy srcuv run bandit -r srcuv run pip-auditThese checks provide automated coverage for:
- application behavior
- formatting
- linting
- type correctness
- common Python security issues
- known vulnerable dependencies
Implemented:
- containerized application environment
- Flask REST API
- PostgreSQL persistence
- Vite frontend
- JWT login
- JWT refresh
- authenticated note CRUD
- user-scoped note ownership
- database migrations
- backend integration tests
- CI quality checks
- SAST scanning
- dependency vulnerability auditing
Not currently implemented:
- public user registration
- team membership
- shared notes
- pagination
- tags
- folders
- search
These features are outside the current baseline and can be added as the application grows.
See LICENSE for licensing information.