A self-hosted, role-aware time tracker for the people who keep meaning to start one.
- What it is
- Project journey
- Features
- Architecture at a glance
- Tech stack
- Project layout
- Getting started
- Configuration
- API surface
- Testing
- Roadmap
- Contributing
- Authors
Micro Time Management is a full-stack web app for tracking how you actually spend your day. You log in, pick a date, and add activities — time blocks with a name, description, and start/end times. The backend rejects overlapping blocks, keeps them in chronological order, and scopes everything to the signed-in user. Admins can manage roles from a separate panel.
The project started as a side-project during a Master's degree and is now being finished as a portfolio piece. It is deliberately built around a real auth stack, a real role model, and real test coverage rather than a toy backend, so it doubles as a reference for anyone wiring up Spring Security + JWT sessions against PostgreSQL.
The bones of this app — the data model, the auth flow, RBAC, the activity logic, the React shell, and most of the early controllers and services — went in back in late 2022 during my Master's. After that the repo sat quiet for a while.
I picked it back up in early 2026 and spent a couple of months getting the foundation back in shape before adding anything new. That stretch was all solo: refactoring the auth flow, isolating the service boundaries, fixing RBAC end-to-end, building out role CRUD with pagination, getting a working user profile + update endpoint in, wiring up the login and registration screens, and giving the test suite some real display names plus a few more supporting tests.
Once the base felt solid again, I brought Claude in as a co-author to finish the rest. Pair-programming with an AI is becoming a normal part of how software gets built, and I wanted to grow into that workflow rather than work around it. I'm still the one driving the product and reviewing every change; Claude just lets me move at a pace I actually enjoy. The auth hardening, the activity tracker UI, the profile page, the admin panel, and most of the service-layer test coverage came out of that pairing.
The result is an app that's mine in design and direction, built with a workflow I'd happily reach for on the next thing too.
- 🔐 Registration & login — BCrypt-hashed passwords, JWT access (5h) + opaque refresh (7d) tokens, automatic refresh on
401. - 📅 Daily activity tracker — create, edit, delete activities per date. Overlapping time ranges and duplicates are rejected at the service layer; insertions stay chronologically sorted.
- 👤 Profile management — update name, email, username, date of birth, and change password with current-password verification.
- 🚪 Logout & session revocation — server-side session is invalidated on logout (and on re-login: single active session per user).
- 🛡️ Role management — create, rename (with name-conflict detection), and soft-delete roles from a dedicated admin panel.
- 👥 Bulk user/role operations — backend endpoints for adding/removing roles to users in bulk (UI coming soon).
- 🔎 Paginated user listing — sortable, scopable, locked behind
ROLE_MTM_ADMIN_OPS.
- 🧪 Service-layer test coverage for activities, users, roles, auth, and pagination utilities.
- 🪵 Rolling-file structured logging via Logback.
- 📓 Swagger UI auto-generated in the dev profile.
- 🔁 Single-flight 401 refresh — concurrent expired requests share one refresh round-trip in the Axios client.
- 🗂 Cross-tab auth sync in the frontend via
storageevents.
┌──────────────────────────────────────┐
│ React SPA │
│ Pages: Home · Login · Register │
│ Dashboard · Activity · │
│ Profile · Admin │
│ ProtectedRoute / AdminRoute │
│ Axios apiClient (+401 refresh) │
└──────────────────┬───────────────────┘
│ HTTPS / JSON
▼
┌─────────────────────────────────────────────────────────────┐
│ Spring Boot 3.5 API │
│ │
│ MtmSessionFilter ──► AuthN/AuthZ ──► Controllers ──► Svcs │
│ │ │ │
│ ▼ ▼ │
│ JWT validate + session DB check Domain logic │
│ │
└────────────────────────────┬────────────────────────────────┘
│ Spring Data JPA
▼
┌───────────────────────────────────────┐
│ PostgreSQL │
│ users · roles · sessions · tokens │
│ micro_activity_record │
└───────────────────────────────────────┘
- Client hits an endpoint with
Authorization: Bearer <jwt>. MtmSessionFilterparses the JWT, callsAuthenticationAndAuthorizationService.validateCurrentUserSessionForAccessToken, and looks up the access token + session in PostgreSQL.- Valid → the resolved
Userbecomes the Spring Security principal for the rest of the request. Expired/invalid →401. Unexpected exception →500(deliberately distinct from auth failures). - Controllers route to services, which return either DTOs or a
GenericMessageResponseDTOenvelope.
| Concern | Choice |
|---|---|
| Language | Java 17 |
| Framework | Spring Boot 3.5.10 |
| Persistence | Spring Data JPA / Hibernate → PostgreSQL (H2 in-memory for tests) |
| Security | Spring Security 6 + custom MtmSessionFilter |
| Auth tokens | jjwt 0.13.0 (HS512) + DB-backed sessions / refresh tokens |
| Validation | spring-boot-starter-validation (Bean Validation) |
| API docs | springdoc-openapi 2.8.15 (Swagger UI) |
| Build | Maven, fat-jar via spring-boot-maven-plugin |
| Logging | Logback (rolling file) |
| Testing | JUnit 5, Mockito, AssertJ |
| Concern | Choice |
|---|---|
| Framework | React 18 (Create React App) |
| Routing | React Router v6 |
| Styling | Tailwind CSS 3 (prefix mtm-) + a custom CSS-variable design system (emerald/teal, light + dark) |
| Fonts | Inter (body) + Sora (display) |
| HTTP | Axios (shared client with request + response interceptors) |
| State | useState + a focused useAuth hook + a tiny pub/sub on top of localStorage |
| Theme | useTheme hook — persisted light/dark toggle with no-flash boot |
| Icons | react-icons |
| Concern | Choice |
|---|---|
| Full stack | Docker Compose (PostgreSQL + backend + frontend + Adminer DB UI) |
| Images | Multi-stage Dockerfiles — backend (Maven → JRE), frontend (Node → nginx) |
| CI | GitHub Actions (.github/workflows/ci.yml) — backend tests on in-memory H2 (no DB service) + frontend build/test |
| Profiles | dev (port 8080, context /mtm-dev) · prod (env-var driven, locked CORS) |
MicroTimeManagement/
├── backend/api-service/ Spring Boot REST API
│ ├── src/main/java/.../apiservice/
│ │ ├── callbacks/ BeforeConvertCallback (audit fields)
│ │ ├── config/ Profile-aware Spring Security
│ │ ├── constants/ Centralised error / role / pagination constants
│ │ ├── controller/ Auth · User · Role · Activity · Admin
│ │ ├── converter/ Model ↔ DTO
│ │ ├── dto/{entity,request,response}
│ │ ├── enums/
│ │ ├── exceptions/ Custom hierarchy + @ControllerAdvice
│ │ ├── filter/ MtmSessionFilter (per-request session validation)
│ │ ├── handler/ Global exception handler
│ │ ├── model/ JPA entities (PostgreSQL)
│ │ ├── repository/ Spring Data interfaces
│ │ ├── service/{,impl} Business logic
│ │ └── utils/ JWT + API utilities
│ └── src/test/java/.../apiservice/ Service-layer tests + test factories
├── frontend/ React 18 SPA
│ └── src/
│ ├── Pages/ Home · Login · Registration · Dashboard ·
│ │ Activity · Profile · Admin
│ ├── components/ ProtectedRoute · AdminRoute · NavigationBar ·
│ │ ThemeToggle · Toast · Footer
│ ├── hooks/ useAuth (auth + profile/roles) · useTheme (light/dark)
│ ├── service/ AuthStorage (token store + pub/sub),
│ │ ApiService (shared axios + endpoint wrappers)
│ └── style/ Tailwind + CSS-variable design system (light/dark)
│ ├── Dockerfile Multi-stage build → nginx
│ └── nginx.conf SPA fallback
├── .github/workflows/ci.yml GitHub Actions CI
├── docker-compose.yml Full stack: PostgreSQL + backend + frontend + Adminer
└── README.md You are here.
- Java 17+ (Temurin / Adoptium recommended)
- Node.js 18+ and npm 9+
- Docker & Docker Compose (for PostgreSQL locally)
- Maven is bundled via
./mvnw; no global install needed
git clone git@github.com:vishu221b/MicroTimeManagement.git
cd MicroTimeManagementdocker compose up --buildThat builds and starts everything:
| Service | Port | What it is |
|---|---|---|
mtm_frontend |
3000 |
React SPA (nginx) at http://localhost:3000 |
mtm_backend |
8080 |
Spring Boot API at http://localhost:8080/mtm-dev |
mtm_postgres |
5432 |
PostgreSQL (data persisted in the mtm_postgres_data volume) |
mtm_adminer |
8081 |
Adminer DB UI at http://localhost:8081 (System: PostgreSQL, Server: mtm_postgres, user/pass/db: mtm/mtm/mtm_dev) |
- Swagger UI: http://localhost:8080/mtm-dev/swagger-ui/index.html
- Health endpoint (open): http://localhost:8080/mtm-dev/actuator/health
Overridable env: MTM_JWT_SECRET, MTM_CORS_ORIGINS, SPRING_DATASOURCE_URL / SPRING_DATASOURCE_USERNAME / SPRING_DATASOURCE_PASSWORD (backend) and the REACT_APP_API_BASE_URL build arg (frontend). Defaults work out of the box on localhost.
Run just PostgreSQL in Docker, then the apps directly.
docker compose up -d mtm_postgres # PostgreSQL on :5432cd backend/api-service && ./mvnw spring-boot:run # API on :8080/mtm-devcd frontend && npm install && npm start # SPA on :3000The dev server runs on http://localhost:3000 and talks to the backend at http://localhost:8080/mtm-dev/api/v1.
The dev profile (application-dev.yml) ships with sensible defaults so you can mvn spring-boot:run after docker compose up. The prod profile (application-prod.yml) requires every secret to come from the environment.
| Variable | Scope | Default | Purpose |
|---|---|---|---|
SPRING_PROFILES_ACTIVE |
both | dev |
Switches between dev and prod config |
MTM_JWT_SECRET |
dev (optional) / prod (required) | bundled dev fallback | Symmetric key for HS512 JWT signing |
SPRING_DATASOURCE_URL |
prod (dev optional) | dev: local Postgres | JDBC URL, e.g. jdbc:postgresql://host:5432/mtm |
SPRING_DATASOURCE_USERNAME |
prod (dev optional) | dev: mtm |
Postgres user |
SPRING_DATASOURCE_PASSWORD |
prod (dev optional) | dev: mtm |
Postgres password |
LOG_FILE_PROPERTIES |
prod | — | Path for rolling log file |
The dev JWT secret has been left in the repo intentionally so the app boots out of the box. Override it in any environment you don't fully control.
Reminders always fire in-app + browser notifications while MTM is open (no setup). The email channel is stubbed: LoggingEmailSender just logs [EMAIL STUB — not actually sent], and a @Scheduled job (ReminderEmailScheduler, every 60s — tune with MTM_REMINDERS_EMAIL_POLL_MS) marks due email-reminders as processed so nothing double-sends.
To send real email (before or after deploy):
- Add the dependency to
backend/api-service/pom.xml:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
- Provide SMTP config via env (e.g. in
docker-compose.ymlundermtm_backend):(SendGrid/Mailgun/Amazon SES all expose an SMTP endpoint that fits these.)SPRING_MAIL_HOST=smtp.yourprovider.com SPRING_MAIL_PORT=587 SPRING_MAIL_USERNAME=apikey-or-user SPRING_MAIL_PASSWORD=secret SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH=true SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE=true - Add a
@Primary @Componentimplementingcom.microtimemanagement.apiservice.email.EmailSenderbacked by Spring'sJavaMailSender— it overrides the stub with no other code changes:@Primary @Component @RequiredArgsConstructor class SmtpEmailSender implements EmailSender { private final JavaMailSender mail; public void send(String to, String subject, String body) { var msg = new SimpleMailMessage(); msg.setTo(to); msg.setSubject(subject); msg.setText(body); mail.send(msg); } }
There's a Free/Pro plan model (Subscription, BillingService, /api/v1/billing) with a Plan card on the Profile page. Billing is stubbed: with no STRIPE_SECRET_KEY set, "Upgrade" simulates a Pro upgrade so the paid-tier flow is testable — no real charge happens.
To enable real Stripe subscriptions (before or after deploy):
- Add the Stripe SDK to
backend/api-service/pom.xml(com.stripe:stripe-java). - Set env on
mtm_backend:STRIPE_SECRET_KEY=sk_live_…and your recurringSTRIPE_PRICE_ID=price_…. - In
BillingServiceImpl.checkout(), replace the "simulate" branch with a real Stripe Checkout Session creation (return its URL — the frontend already redirects tocheckoutUrlwhen present), and add a webhook endpoint to flip the plan toPROoncheckout.session.completed/ back toFREEon cancellation.
A trimmed map of the v1 API — full schemas live in the Swagger UI.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/api/v1/auth/login |
public | Username/email + password → access + refresh tokens |
POST |
/api/v1/auth/refresh |
public | Refresh token → new access token |
POST |
/api/v1/auth/logout |
USER_OPS |
Revoke current session |
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/api/v1/user/register |
public | Create new account (default roles: USER_OPS, ACTIVITY_CRUD) |
GET |
/api/v1/user/profile |
USER_OPS |
Current user, including roles |
PUT |
/api/v1/user/update |
USER_OPS |
Update own first/last name, email, username, DOB |
POST |
/api/v1/user/resetPassword |
USER_OPS |
Verify old password, set new password (≥ 8 chars) |
DELETE |
/api/v1/user/delete |
USER_OPS |
Soft-delete current account |
GET |
/api/v1/user/getByUserId |
ADMIN_OPS |
Look up any user by UID |
GET |
/api/v1/user/all |
ADMIN_OPS |
Paginated, sortable user listing |
POST |
/api/v1/user/addRole |
ADMIN_OPS |
Bulk add roles by uid/email/username |
DELETE |
/api/v1/user/removeRole |
ADMIN_OPS |
Bulk remove roles |
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/api/v1/activity |
ACTIVITY_CRUD |
Create / append activity (overlap-validated) |
GET |
/api/v1/activity/getAllForDate?date= |
ACTIVITY_CRUD |
Activities for a date, scoped to current user |
PUT |
/api/v1/activity?date= |
ACTIVITY_CRUD |
Update an activity (metadata-only or full re-time) |
DELETE |
/api/v1/activity?date=&recordId= |
ACTIVITY_CRUD |
Remove a single activity from a day's record |
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/api/v1/role |
ROLE_CRUD |
Create role |
GET |
/api/v1/role |
ROLE_CRUD |
List or get by id (paginated) |
PUT |
/api/v1/role |
ROLE_CRUD |
Rename role (conflict-checked) |
DELETE |
/api/v1/role?roleId= |
ROLE_CRUD |
Soft-delete role |
cd backend/api-service
./mvnw test # full suite
./mvnw test -Dtest=ActivityRecordServiceImplTest # one classService-layer coverage today (selected highlights):
| Suite | Notes |
|---|---|
ActivityRecordServiceImplTest |
14 cases — create / append / overlap / duplicate / invalid date / get / delete / update (metadata + retime + overlap rejection) |
UserServiceImplTest |
Registration, lookup, profile fetch, update (with cross-uid IDOR rejection), password change happy + wrong-old-password rejection |
RoleServiceImplTest |
8 cases — create / get / list / soft-delete / rename happy / no-op / conflict / not-found |
AuthenticationAndAuthorizationServiceImplTest |
Login happy + bad password |
ApiUtilsTest |
Pagination sanitisation edge cases |
The whole suite is green (79 tests). It runs against in-memory H2 (
MODE=PostgreSQL), somvn testand CI need no external database.
cd frontend
npm run build # production bundle (also runs ESLint)
npm test # Jest in watch mode (test coverage is the next milestone)- ✅ Auth flow with single-flight refresh, protected + admin routes
- ✅ Activity tracker CRUD UI
- ✅ Profile + password change UI
- ✅ Admin role management UI
- 📊 Dashboard with recent-activity summary
- 🧪 React Testing Library coverage for pages + hooks
- 👥 User → Role assignment UI in the admin panel
- 📆 Migrate string dates to
LocalDateand swap the DOB text input for a date picker - ⚡ Bulk role lookup (eliminate the per-request N+1 in
UserServiceImpl.replaceRoleIdsWithNamesForUser) - 🐳 Wire the prod Docker Compose app service with env-var-driven config
- In-app AI chat — a chat panel inside MTM that talks to an LLM (Anthropic at MVP, pluggable later) and uses task-shaped tools to act on the user's behalf — logging activities, summarising windows, looking up the day's entries. Auth is the user's normal MTM session, so there's no new token type, no new protocol surface. Single-turn-with-tools by design (no chatbot rabbit-hole): no persisted conversation memory across sessions, no general chit-chat fallback. Per-user daily token cap on an app key, with optional BYOK so power users can lift the cap with their own Anthropic key.
Architecture, premortem (the failure modes we're deliberately designing around), phasing, and the full task list live in CLAUDE.md under "AI Chat Integration (Planned, not started)".
A complete pending list lives in CLAUDE.md.
This is a personal portfolio project, but PRs and issues are welcome.
- Fork → branch (
feat/<thing>). - Backend changes ship with tests. Service-layer logic is TDD-friendly — write the test first.
- Run
./mvnw testandnpm run buildbefore pushing. - Open a PR against
mainwith a description of what changed and why.
The development conventions (package layout, exception hierarchy, response wrappers, callback contracts, etc.) are documented in CLAUDE.md and updated alongside every feature.
- Vishal Dogra — design, product direction, original build, review of every change.
- Claude (Anthropic) — co-author. Paired on the later milestones: auth hardening, the activity tracker UI, the profile + password flows, the admin role panel, and most of the service-layer test coverage.
If this project taught you something, or you spot a bug, please open an issue — feedback is the entire point of shipping it publicly.