A lightweight fitness-tracking app with a Go backend (Gin + GORM), Postgres & Redis, and a React (Vite + Tailwind) frontend.
Track workouts, exercises, and progress. The backend exposes a REST API with JWT auth; Postgres persists core data, and Redis powers caching, sessions, and rate limiting. The frontend is a responsive SPA built with React, Vite, and Tailwind.
- Backend: Go, Gin, GORM, JWT
- Databases: Postgres, Redis
- Frontend: React, Vite, Tailwind CSS
- Tooling: Docker Compose
- External APIs: OpenAI, Deepl Translate, Sendgrid, AWS
General structure
.
├── .github/
│ └── workflows/
│ └── deploy.yml
│
├── backend/
├── frontend/
│
├── .env.dev
├── .env.prod
├── .gitignore
├── docker-compose.yml
└── docker-compose.prod.ymlBackend
backend/
├── cmd/server/main.go # Entrypoint of the app
├── internal/
│ ├── domain/ # Domain models, services, use-cases
│ │ ├── ai/
│ │ ├── email/
│ │ ├── errors/ # Custom error definitions
│ │ ├── events/
│ │ │ └── handlelog.go # Handle log definition
│ │ ├── shared/
│ │ │ └── domainevt
│ │ │ └── domain.go # Domain event definition
│ │ ├── user/
│ │ │ ├── profile.go # Domain definition of entity
│ │ │ ├── repository.go # Domain definition of repository
│ │ │ └── user.go
│ │ ├── versions/ # Domain definition of versions for the app
│ │ ├── rbac/
│ │ ├── translations/
│ │ └── workout/
│ │
│ ├── infrastructure/
│ │ ├── ai/ # AIs
│ │ │ └── openai.go
│ │ ├── db/
│ │ │ ├── postgres/ # Data layer of the app
│ │ │ │ ├── db.go # All migrations and layer assembly
│ │ │ │ ├── seed_rbac.go # Seeding roles and permissions
│ │ │ │ ├── exercise_repo.go
│ │ │ │ ├── user_repo.go
│ │ │ │ └── ...
│ │ │ ├── txctx/
│ │ │ └── txctx.go # Helper to pick up transactions
│ │ │ └── redis/ # Rate limiter
│ │ │ └── limiter.go
│ │ ├── email/ # Email bots, etc.
│ │ │ └── gmail_sender.go
│ │ ├── translations/ # Translations APIs
│ │ │ └── deepl_translator.go
│ │ ├── eventbus/
│ │ │ └── bus.go
│ │ ├── uow/
│ │ │ ├── accumulator.go # Events accumulator
│ │ │ └── uow.go # Transactions helper
│ │ └── job/ # Cleanups
│ │ └── cleanup.go
│ │
│ ├── events/
│ │ │── idem/
│ │ │ └── try_process.go # Idempotency logic (events logging)
│ │ │── workout/
│ │ │ └── handler.go # Subscriptions for workouts
│ │ └── registry.go # Registry of all subscriptions
│ │
│ ├── interface/http/
│ │ ├── dto/ # DTOs
│ │ │ ├── mappers.go
│ │ │ ├── ai_dto.go
│ │ │ ├── user_handler.go
│ │ │ └── ...
│ │ ├── handler/ # Handlers defining routes
│ │ │ ├── helpers.go
│ │ │ ├── ai_handler.go
│ │ │ ├── exercise_handler.go
│ │ │ ├── user_handler.go
│ │ │ └── ...
│ │ ├── middleware/ # Middlewares of the app
│ │ │ ├── jwt.go
│ │ │ ├── rbac.go
│ │ │ └── rate_limit.go
│ │ └── server.go # Server file that registers all handlers
│ |
│ └── usecase/ # Service layer of the app
│ ├── contracts.go # Definition of service structures
│ ├── user/
│ │ ├── service.go # Service initialization
│ │ ├── user_service.go # Service functions
│ │ ├── consent_service.go
│ │ └── ...
│ ├── ai/
│ │ ├── ai_service_test.go # Go tests
│ │ ├── helpers_test.go
│ │ ├── service.go
│ │ ├── ai_service.go
│ │ └── helpers.go
│ ├── email/
│ │ ├── service.go
│ │ ├── email_service.go
│ │ └── helpers.go
│ ├── rbac/
│ │ ├── service.go
│ │ └── rbac_service.go
│ ├── translations/
│ │ ├── service.go
│ │ ├── helpers.go
│ │ └── translation_service.go
│ ├── exercise/
│ │ ├── service.go
│ │ ├── muscle_group_service.go
│ │ └── exercise_service.go
│ └── workout/
│ ├── service.go
│ ├── workout_plan_service.go
│ ├── workout_cycle_service.go
│ └── ...
│
├── docs/docs.go # Swagger, etc.
├── Dockerfile
├── Dockerfile.prod
└── .dockerignoreFrontend
frontend/
├── public/
│ ├── screenshots/ # For PWA preview
│ ├── robots.txt
│ ├── favicon.ico
│ └── ...
│
├── src/
│ ├── components/
│ │ ├── admin/
│ │ ├── workout/
│ │ └── ...
│ ├── context/
│ │ └── AuthContext.jsx # Context for authentication
│ ├── forms/
│ │ ├── login/
│ │ │ ├── RegisterForm.jsx
│ │ │ ├── LoginForm.jsx
│ │ │ └── ...
│ │ ├── workout/
│ │ ├── profile/
│ │ ├── admin/
│ │ └── ...
│ ├── hooks/
│ │ ├── data/
│ │ │ ├── useCycleData.jsx
│ │ │ └── ...
│ │ ├── useConsent.jsx
│ │ ├── useNetworkBanner.jsx
│ │ └── ...
│ ├── icons/
│ │ ├── AddIcon.jsx
│ │ ├── CheckIcon.jsx
│ │ └── ...
│ ├── layout/
│ │ ├── Footer.jsx
│ │ ├── Header.jsx
│ │ └── Layout.jsx # Layout of the pages
│ ├── locales/ # Translations
│ │ ├── en/
│ │ ├── ru/
│ │ └── zh/
│ ├── modals/
│ │ ├── admin/
│ │ ├── workout/
│ │ └── ...
│ ├── pages/
│ │ ├── admin/
│ │ ├── policies/
│ │ ├── workout/
│ │ │ ├── WorkoutPlans.jsx
│ │ │ └── ...
│ │ ├── Home.jsx
│ │ └── ...
│ ├── states/ # Pages showing states
│ │ ├── ErrorState.jsx
│ │ ├── LoadingState.jsx
│ │ └── GlobalLoadingState.jsx
│ ├── styles/
│ │ └── index.css
│ ├── utils/ # Helpers
│ ├── api.js # Axios interceptors, etc.
│ ├── App.jsx # Routes, etc.
│ ├── i18n.js
│ ├── sw.js # Service worker (Vite PWA)
│ └── main.jsx
│
├── tests/
│ ├── __mocks__/
│ ├── utils/
│ ├── useCycleData.basic.test.jsx
│ └── ... # Other tests
│
├── index.html
├── nginx.conf
├── package.json
├── package-lock.json
├── vite.config.js
├── vitest.config.js
├── .dockerignore
├── Dockerfile
└── Dockerfile.prod- Only one active workout plan per user.
- When a user creates a new plan, it becomes active and other plans are deactivated.
- Structure is essentially a linked list.
- A cycle is considered current if next_cycle_id is nil.
- The first cycle cannot be deleted.
- When a cycle is between two cycles, the service layer bridges prev <-> next.
- If a cycle is the last one, detach next from prev and delete the node.
- If the opened cycle has no workouts but the previous cycle does, copy the workouts from the previous cycle into the opened (subject for change - better to move copying to the new cycle creation)
- Allowed to be empty and may be sent as an empty field to the backend on blur.
- Can be checked only if not null (weight can be 0) and if reps and weight are within the limits.
- When the user changes a value, it should only update the cache and perform soft checks.
- If it's currently checked, send a request to the backend to uncheck it.
- When the user blurs the field, send a request to the backend to update the value (reps or weight); the backend should perform a soft check.
- When an update happens, the backend automatically sets skipped = false and completed = false.
- When the user toggles the checkbox to check it, perform a hard validation.
- Unchecking should happen regardless and be followed by a request to the backend.