Secure Notes Vault API

System overview

Secure Notes Vault API is a Spring Boot REST service for creating, managing, and sharing private notes. Users register and log in with username/password credentials, then authenticate subsequent requests with a JWT bearer token.

The application is organized around a standard controller-service-repository flow:

  • Controllers expose the authentication and notes REST endpoints.
  • Spring Security and a JWT filter authenticate protected requests.
  • Services enforce note ownership and read-only shared-note access rules.
  • JPA repositories persist users, notes, and note shares.
  • Flyway migrations manage the database schema for both local H2 and PostgreSQL-backed runs.

Running the program

This project requires Java 21 and Maven. The API starts on http://localhost:8080.

For the fastest local setup, run the app with the in-memory H2 database:

make dev

To run the test suite:

make test

To run the API and PostgreSQL with Docker Compose:

make docker-up

Stop the Docker Compose stack with:

make docker-down

You can also run the Spring Boot app directly:

JWT_SECRET=dev-secret-change-me-do-not-use-in-production mvn spring-boot:run

Design decisions

  • Java 21 was selected because the challenge is for a Java-oriented backend role and because modern Java records work well for request/response DTOs.
  • Spring Boot provides a compact way to build a production-style REST API with validation, dependency injection, security, persistence, and test support without adding much custom framework code.
  • Maven is used for dependency management and repeatable local builds because it is widely understood in Java backend environments.
  • Spring Security handles authentication boundaries, while a custom JWT filter keeps the API stateless after login. This makes the service easier to run behind a load balancer because requests do not depend on server-side sessions.
  • Passwords are stored as BCrypt hashes, never as plaintext. The JWT signing secret is supplied through configuration and should come from a secret manager or environment variable outside local development.
  • H2 is used for fast local development and automated tests. PostgreSQL is supported through Docker Compose and a Spring profile because it is a better fit for a production relational datastore.
  • JPA/Hibernate keeps the persistence layer concise for this size of application. The schema is still managed explicitly with Flyway so database changes are versioned and repeatable.
  • The data model follows the required User, Note, and NoteShare entities, with an added title field on notes for a more realistic note-taking API.
  • Share records grant read-only access to a specific user. Owners can create, update, delete, and share their notes; shared recipients can only read them.
  • UUID primary keys are used so identifiers are opaque and do not expose record counts or ordering.
  • The service returns structured error responses for application-level failures so clients can handle validation, authentication, authorization, and conflict cases predictably.
  • This implementation favors clarity over extra infrastructure. Features such as refresh tokens, token revocation, audit logging, pagination, rate limiting, and OpenAPI generation would be good production follow-ups.

Security considerations

  • Passwords are hashed with BCrypt before being persisted. Plaintext passwords are accepted only in registration/login request bodies and are never returned by the API.
  • Authentication uses JWT bearer tokens. Protected endpoints require an Authorization: Bearer <token> header.
  • The JWT signing secret is supplied through configuration. The local development value should not be used in production; production deployments should load it from an environment variable or secret manager.
  • Access control is enforced in the service layer. Users can read and modify only their own notes unless a note has been explicitly shared with them.
  • Shared notes are read-only for recipients. Only the owner can update, delete, or share a note.
  • The API returns 404 Not Found for another user's unshared note so callers cannot distinguish between a missing note and a note they are not allowed to see.
  • Production deployments should run behind HTTPS, avoid logging credentials or tokens, and consider token expiration, rotation, revocation, rate limiting, and audit logging.

Assumptions, tradeoffs, and future improvements

  • Usernames are treated as unique login identifiers. Email verification, password reset flows, and account recovery are outside the scope of this challenge.
  • Note sharing is user-to-user and read-only. The implementation does not include groups, roles, expiring shares, or write permissions for shared users.
  • H2 is used for fast local development and tests, while PostgreSQL is available through Docker Compose for a more production-like relational database.
  • JWT authentication keeps the API stateless, but this implementation does not include refresh tokens, token revocation, or a logout mechanism.
  • The API currently returns all visible notes from GET /notes. Pagination, sorting, filtering, and search would be important as the dataset grows.
  • Additional production-grade coverage would include update/delete/share mutation tests, PostgreSQL integration tests, and coverage reporting.
  • Future improvements could include OpenAPI documentation, audit logging, rate limiting, structured request logging, health checks, metrics, and CI/CD automation.

Production considerations

  • Deployment: package the Spring Boot service as a container, run it behind HTTPS, and connect it to a managed PostgreSQL database. Secrets such as JWT_SECRET should come from the deployment platform's secret manager rather than source control or image defaults.
  • Monitoring and alerts: track request latency, 4xx/5xx rates, authentication failures, database connection pool health, migration failures, and unexpected exception counts. Alert on sustained 5xx responses, database connectivity issues, and spikes in failed logins.
  • Database migrations: continue using Flyway versioned migrations. In production, migrations should be reviewed, tested against staging data, and applied as part of a controlled deployment process.
  • Scaling: the API is stateless after login, so multiple application instances can run behind a load balancer. To support higher concurrency, tune the database connection pool, add pagination to list endpoints, keep indexes aligned with access patterns, and consider caching only after measuring bottlenecks.

Testing

The automated tests run with Spring Boot, MockMvc, Spring Security, JPA, validation, and Flyway-backed H2 enabled. This keeps the tests close to real API behavior while still making them fast enough to run locally.

Run the test suite with:

make test

Current automated coverage includes:

  • Registration creates users, hashes passwords, rejects duplicate usernames, and returns validation errors for invalid payloads.
  • Note creation requires a bearer token, persists notes for the authenticated owner, and rejects invalid note payloads.
  • Note listing returns only notes owned by the caller plus notes explicitly shared with the caller.
  • Note retrieval returns owned notes, returns shared notes as SHARED_READ, and verifies that a user cannot access another user's unshared note.
  • Protected endpoints return 401 Unauthorized when no JWT is supplied.
  • Invalid UUID path variables return a structured 400 Bad Request response.

The required access-control test is covered by NoteRetrievalControllerTest.getNoteReturnsNotFoundForUnsharedNoteOwnedByAnotherUser, which creates a note as one user, authenticates as a different user, and asserts that the second user receives 404 Not Found for the unshared note.

Additional production-grade coverage would include update/delete/share mutation tests, PostgreSQL integration tests, and coverage reporting.

A React app located at https://git.seanstarkey.dev/starkey/notes was also used for manual API testing. It exercises the API from a client perspective and displays REST requests/responses for debugging. It runs in a Docker container. This app was worth the investment because of the problems I had with some REST communications.

API Endpoints

Authentication

Method Endpoint Description
POST /auth/register Register a new user
POST /auth/login Authenticate and receive a JWT

Notes

Method Endpoint Description
POST /notes Create a new note
GET /notes List owned notes and notes shared with you
GET /notes/{id} Get a note by ID (owner or shared recipient)
PUT /notes/{id} Update a note (owner only)
DELETE /notes/{id} Delete a note (owner only)
POST /notes/{id}/share Share a note with another user (read-only)

API Usage Examples

The following examples assume the API is running at http://localhost:8080.

Register two users:

curl -i -X POST http://localhost:8080/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"username":"alice","password":"password123"}'

curl -i -X POST http://localhost:8080/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"username":"bob","password":"password123"}'

Log in and save each JWT:

ALICE_TOKEN=$(curl -s -X POST http://localhost:8080/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"alice","password":"password123"}' | jq -r '.token')

BOB_TOKEN=$(curl -s -X POST http://localhost:8080/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"bob","password":"password123"}' | jq -r '.token')

Create a note as Alice:

NOTE_ID=$(curl -s -X POST http://localhost:8080/notes \
  -H "Authorization: Bearer $ALICE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"title":"Launch checklist","content":"Rotate credentials before launch."}' | jq -r '.id')

List Alice's visible notes:

curl -i http://localhost:8080/notes \
  -H "Authorization: Bearer $ALICE_TOKEN"

Share Alice's note with Bob:

curl -i -X POST http://localhost:8080/notes/$NOTE_ID/share \
  -H "Authorization: Bearer $ALICE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"username":"bob"}'

Bob can read the shared note:

curl -i http://localhost:8080/notes/$NOTE_ID \
  -H "Authorization: Bearer $BOB_TOKEN"

Bob cannot update the shared note because shared access is read-only:

curl -i -X PUT http://localhost:8080/notes/$NOTE_ID \
  -H "Authorization: Bearer $BOB_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"title":"Edited by Bob","content":"This update should be rejected."}'

HTTP return codes

Application-defined HTTP status codes are generated in AuthController for successful registration and in GlobalExceptionHandler for API error responses. Spring Security and Spring MVC may still produce framework-level responses for requests that do not reach a controller, such as unauthenticated protected requests or unsupported methods.

HTTP code Status Generated in Trigger Response body
201 Created AuthController.register POST /auth/register creates a new user. UserResponse with id, username, and createdAt.
400 Bad Request GlobalExceptionHandler.handleMalformedJson Request body cannot be parsed or converted from JSON. ErrorResponse with Malformed request body.
400 Bad Request GlobalExceptionHandler.handleTypeMismatch Path variable or query parameter cannot be converted to the expected type. ErrorResponse with Invalid request parameter.
401 Unauthorized GlobalExceptionHandler.handleAuthentication; Spring Security filter chain Authentication fails or an unauthenticated request targets a protected route. ErrorResponse with Invalid username or password when handled by GlobalExceptionHandler; framework-generated body when rejected before controller handling.
403 Forbidden GlobalExceptionHandler.handleForbidden Authenticated caller is not allowed to perform the requested operation. ErrorResponse with the exception message.
404 Not Found GlobalExceptionHandler.handleNotFound Requested domain resource does not exist or should be hidden from the caller. ErrorResponse with the exception message.
404 Not Found GlobalExceptionHandler.handleNoResourceFound No controller route or static resource matches the request. ErrorResponse with Not found.
409 Conflict GlobalExceptionHandler.handleConflict; AuthController.register throws ConflictException Duplicate username or other persistence/domain uniqueness conflict. ErrorResponse with the exception message.
422 Unprocessable Entity GlobalExceptionHandler.handleValidation Bean Validation fails for a request body annotated with @Valid. ValidationErrorResponse with error and per-field fields.

Database

  • The schema is the basic requirements for a User, Note and NoteShare.
  • H2 database is used for development.
  • Flyway will allow us to migrate to Postgres for "production".
Table Column Type Required Key / Constraint Description
users id UUID Yes Primary key Unique user identifier.
users username VARCHAR(100) Yes Unique: uq_users_username User login handle.
users password_hash VARCHAR(255) Yes BCrypt password hash.
users created_at TIMESTAMP WITH TIME ZONE Yes Account creation timestamp.
notes id UUID Yes Primary key Unique note identifier.
notes owner_id UUID Yes FK to users(id); indexed by idx_notes_owner_id User who owns the note.
notes title VARCHAR(255) Yes Note title.
notes content TEXT Yes Note body content.
notes created_at TIMESTAMP WITH TIME ZONE Yes Note creation timestamp.
notes updated_at TIMESTAMP WITH TIME ZONE Yes Last note update timestamp.
note_shares id UUID Yes Primary key Unique share identifier.
note_shares note_id UUID Yes FK to notes(id) with ON DELETE CASCADE; indexed by idx_note_shares_note_id Note being shared.
note_shares shared_with_user_id UUID Yes FK to users(id); indexed by idx_note_shares_shared_with_user_id User receiving read-only access.
note_shares created_at TIMESTAMP WITH TIME ZONE Yes Share creation timestamp.
note_shares note_id, shared_with_user_id UUID, UUID Yes Unique: uq_note_shares_note_user Prevents duplicate shares for the same note and user.

References

A list of projects looked at for reference.

Description
Secure Notes Vault API for Bluestaq
Readme 185 KiB
Languages
Java 98.2%
Makefile 0.9%
Dockerfile 0.5%
Shell 0.4%