This is a full-stack weight tracking application that:
- Fetches weight data from external sources (Google Fit API), cleans, stores, and aggregates it
- Allows user to manually add or remove their daily weight entries
- Presents weekly trends and weight goal-based insights in a modern web UI
I built it as a portfolio project to demonstrate end-to-end skills: full-stack development, OAuth 2.0, 3rd-party API integration, modular backend architecture, automated testing, and CI/CD.
You can sign up for a new account or use the demo credentials below:
Demo credentials
- Email:
wtdemo@justas.tech - Password:
demo1
Note on Demo mode and Google Fit Integration: The credentials above give access to demo mode where Google Fit integration is replaced by mock data source. The app is currently in the testing phase on Google’s platform, so only the testing group users can sync the app with their Google Fit data. If you want to test live integration, I can add you to the testing group - contact me mentioning if you signed up on the app and including your gmail address. Otherwise, the complete app with Google Fit integration can be run and tested locally.
- Scope: Full-stack SPA (React + TypeScript frontend and Python/FastAPI backend) with Google OAuth 2.0 + Google Fit API integration.
- Architecture: REST APIs, protocol-based interfaces, dependency injection, modular architecture, demo mode.
- Key source code:
- Backend (
app/):main.py,api.py,google_fit.py,data_integration.py,analytics.py - Frontend (
ui-react/):App.tsx,src/componentsfolder
- Backend (
- Quality: 85%+ unit test coverage, full business logic coverage via integration tests, static type checking, linting/formatting via pre-commit, GitHub Actions CI.
- Deployment: Backend on Koyeb, frontend on Cloudflare Pages, Supabase for Auth + PostgreSQL.
- Security & Auth: JWT-based auth via Supabase for frontend and APIs, secure Google OAuth 2.0 token storage/refresh to access Google data.
- Live Demo
- Summary for Hiring Teams
- Project Overview
- Features
- Tech Stack
- Architecture & Project Structure
- API Documentation
- CI/CD Pipeline
- Testing
- Running the Project Locally
- Design Patterns & Best Practices
- Limitations & Enhancements
- Contact Me
I built this project because it:
- Solves a real problem – When tracking weight, weekly averages and trends are more helpful than daily fluctuations. Google Fit and similar apps are good for logging data but don't provide a convenient interface for such analysis.
- Presents advanced full-stack challenges – Secure OAuth 2.0 with Google, fetching and processing real-world data from 3rd party APIs, a React SPA powered by REST APIs, analytics on time-series data, automated testing, and production deployment.
This is the first full-stack application I shipped to production. It helped me build various skills such as:
- Full-stack development – React + TypeScript SPA frontend with a Python/FastAPI backend.
- OAuth 2.0 – Implementing the flow to get access to user’s Google Fit data.
- API design – REST endpoints with clear responsibilities and versioning.
- Database design and usage – PostgreSQL + SQLModel for relational data.
- Clean code principles – modular architecture, DRY and SOLID (see Best Practices).
- CI/CD – GitHub Actions, pre-commit hooks, and thorough automated tests.
- Type safety – Python type hints + mypy, and TypeScript for the frontend.
- Cloud deployment – Koyeb (backend) and Cloudflare Pages (frontend), Supabase for managed PostgreSQL + Auth.
Getting Data
- Manual entry - the user can enter their daily weight, view it using dail view and delete it using the app
- Syncing data with external sources - the user can also request to get data from their selected source (currently only Google Fit supported)
Progress Dashboard
- Filtering data
- View data for a specific date range (from/to dates)
- View data for the last N weeks
- Real-time updates when filters change
- Weight change overview over selected period: total change from dates X to Y (in absolute and relative terms), average weekly change, average estimated caloric surplus / deficit
- Weekly details - key metrics for every week in the selected period
- Meaningful metrics - PTs and nutritionists generally recommend keeping weight change to ~0.5%–1% per week for a healthy and sustainable change. The app calculates this rate for the user’s data so they can see if their weight change is within a healthy range.
- Responsive UI - clean minimalistic UI that adjusts to various screen sizes, including mobile
Goal-Based Evaluation
- Switch between three fitness goals: Lose Fat, Maintain Weight, Gain Muscle
- The result metrics are color-coded (positive/negative) based on your selected goal
Authentication & Authorization
- JWT authentication via Supabase to sign up, log in to the frontend, and to access the app's REST APIs
- Google OAuth 2.0 implementation to get access the user's Google Fit data. This includes secure access token storage and refresh mechanism
Data Management
- Data syncing strategy - on user's request, fetch and store their data from the external source (Google Fit)
- Database as source of truth - weight entries fetched from external sources are inserted only if there's no entry for that user and date in the DB yet.
- Flexible storage layer - the app supports multiple storage systems: PostgreSQL and file-based storage
- Analytics module - calculates and returns metrics for the stored weight data
External Integrations
- Google Fit REST API - Fetch user's weight entries
- Supabase Auth - User authentication and JWT validation
Architecture
- RESTful API design with FastAPI
- Protocol-based abstractions for storage layer and external data sources
- Dependency injection for clean, testable code
- Exception handling with meaningful responses and logging
- Type safety with strict MyPy checking
| Technology | Purpose |
|---|---|
| Python 3.12 | Backend logic |
| FastAPI | Framework for REST APIs with automated request/response validation (Pydantic). Includes uvicorn ASGI server |
| Pydantic | Automated data validation for HTTP requests/responses and ORM objects |
| SQLModel | SQL ORM with Pydantic integration |
| PostgreSQL | Relational DB (managed by Supabase in production) |
| Pandas | Data processing and analytics |
| Poetry | Dependency management and packaging |
| Supabase | Protecting APIs with JWT auth |
| Google APIs | OAuth 2.0 and Google Fit integration |
| Technology | Purpose |
|---|---|
| React 19 | SPA frontend built around reusable components and state management |
| TypeScript | Frontend logic |
| Vite | Frontend build tool and dev server |
| Custom HTML/CSS | BEM methodology for maintainability |
| Supabase JS | Authentication client |
| Tool | Purpose |
|---|---|
| Pre-commit | Git pre-commit hooks for code quality (mypy, ruff lint, format) and integration tests |
| MyPy | Static type checking (strict mode) |
| Ruff | Python linter and formatter |
| Pytest | Automated unit and integration testing |
| GitHub Actions | CI / CD pipeline: automated code quality checks and tests |
| Platform | Purpose |
|---|---|
| Koyeb | Backend API hosting |
| Cloudflare Pages | Frontend hosting |
| Supabase Cloud | Managed PostgreSQL + Auth service |
The diagram below presents the overall architecture of the system, including external elements.
The diagram below details the backend architecture - key modules, interfaces and classes, and their dependencies.
weight-tracker/ # Project root (managed with Poetry)
│
├── app/ # Backend (Python / FastAPI)
│ ├── main.py # Application entry point
│ ├── api.py # REST API routes, validation models and helpers
│ ├── google_fit.py # Google OAuth endpoints, token management + Google Fit API integration
│ ├── data_integration.py # Orchestration service to sync storage with external sources
│ ├── db_storage.py # PostgreSQL DataStorage implementation - weight data and tokens storage
│ ├── file_storage.py # File system-based DataStorage implementation - weight data and tokens storage
│ ├── analytics.py # Weekly weight data aggregation and analytics calculations
│ ├── project_types.py # Type definitions and Protocol-based interfaces
│ ├── utils.py # Helper functions
│ ├── demo.py # Demo-mode DataSourceClient implementation
│ └── .env.example # Backend environment variable config template
│
├── ui-react/ # Frontend (React / TypeScript)
│ ├── src/
│ │ ├── main.tsx # Frontend React app entry point
│ │ ├── App.tsx # Root component with global state management
│ │ ├── index.css # Global styles (BEM style)
│ │ ├── components/ # Reusable React components
│ │ └── types/ # Frontend TypeScript type definitions
│ └── package.json # Frontend dependencies (npm)
│ └── vite.config.ts # Vite build tool configuration
│ └── .env.example # Frontend environment variable config template
│
├── tests/ # Backend test suite
│ └── backend/
│ ├── unit/ # Unit tests (module-level)
│ └── integration/ # Integration tests (organized by feature group)
│
├── .github/workflows/ # CI/CD pipeline workflows
│ ├── backend_code_quality.yaml # Code Quality check flow
│ └── backend_testing.yaml # Unit & integration test flow
│
├── doc/ # Documentation assets (architecture diagrams, screenshots)
|
├── README.md # Project documentation (this file)
├── pyproject.toml # Backend dependencies (Managed by Poetry)
├── poetry.lock # Locked Python dependency versions
└── .pre-commit-config.yaml # Pre-commit hooks (linter, formatter, type-check, integration tests)
| Module | Purpose |
|---|---|
| main.py | FastAPI app setup, API route registration |
| api.py | Logic of the main REST API routes powering the app. Includes helper functions, HTTP request / response model validations, user's JWT validation. Uses FastAPI dependency injection to get the relevant data storage, external data source, and database connection instances |
| data_integration.py | DataIntegrationService orchestrates data sync between DB data and external sources. Uses DI to support any implementation of data storage and data source protocols |
| db_storage.py / file_storage.py | Two different implementations of the DataStorage protocol that give CRUD access to stored weight data |
| analytics.py | Analytics engine. Calculates and returns weekly aggregates and summary metrics from daily weight entries using pandas |
| google_fit.py | Logic related to Google OAuth flow and Google Fit API integration. Defines 2 API routes used to handle Google OAuth 2.0 flow (one to redirect user to Google consent flow, another to handle authorization callback from the server). Also, contains GoogleFitAuth class that stores, loads and refreshes OAuth tokens. Finally, the GoogleFitClient class fetches raw data from Google Fit API and transforms it to standard format |
| mfp.py | Integrates with MyFitnessPal as alternative to Google Fit and demonstrates flexibility of DataSourceClient protocol. Currently disabled due to 3rd party library package issues - PR raised to fix it |
| project_types.py | Complex type definitions for type annotation and type safety checks. Includes definitions of the DataStorage and DataSourceClient protocols |
| demo.py | Implements mock implementation of the DataSourceClient protocol with mock source data. It's in demo mode (when logging in with demo user credentials) |
Below are key React components that constitute the frontend (smaller components not mentioned)
| Component | Purpose |
|---|---|
| App.tsx | Root component. Overall layout of the SPA (Header and Main subcomponents), top-level logic and callbacks passed down to sub-components. Includes logic to manage the auth state, triggering toast messages, triggering data sync to refresh the whole app, etc. |
| Header.tsx | Logo, goal selection, current user info and sign out option |
| Authentication.tsx | Page that presents either Login or SignUp subcomponent, allowing user to login or sign up. It's rendered when there is no active authenticated session |
| Main.tsx | Layout of the main section of the app. Also contains filter components and a selection of CTAs to trigger data sync from various sources |
| Summary.tsx | Cards that summarize the weight change over the selected period |
| WeeklyDataTable.tsx | Table with one row for each week in the selected period with key metrics for that week. Color coded results based on selected goal |
| DailyDataTable.tsx | Table with one row for each daily entry in the selected period and an option to delete the entry |
| AddDataModal.tsx | Modal component that allows manually adding new entries |
| Filters.tsx | Controls to select the data period (last N weeks or from dates X to Y) |
The screenshot below shows the layout of the key components in the UI:
All API endpoints require Authorization header with JWT, validated via Supabase Auth. The API routes use the JWT token in the Authorization header to determine which user's data should be read / modified.
Authorization Header:
Authorization: Bearer <supabase_jwt_token>Currently, the APIs can only be accessed from the domain hosting the React Frontend app (defined as an environment variable FRONTEND_URL)
The app is powered by the following endpoints (only data associated with authenticated user is returned)
GET /daily-entries-> the user's daily weight entries stored in DBPOST /daily-entries-> create a new weight entry in DBDELETE /daily-entries-> delete a weight entry in DBGET /weekly-aggregates-> calculates weekly averages and other key metrics grouped by weekGET /summary-> total weight change metrics over a given periodGET /latest-entry-> the latest daily weight entry (for date closest to current date)POST /sync-data-> triggers fetching data from the selected external data source and inserting new entries in the app storageGET /healthz-> API status check
API is prefixed with /api/<version_number>. The latest prefix is included in the API documentation (see below).
Full API documentation is generated automatically by FastAPI and is available at:
The app has 2 endpoints that are used in the Google OAuth 2.0 flow to obtain access tokens that are used to fetch the user's Google Fit data.
Frontend client redirects end user to this endpoint to initiate Google OAuth 2.0 flow. This endpoint prepares the config needed for auth flow and redirects the user to Google consent page.
Google OAuth server calls this endpoint after successful consent from user. This endpoint fetches the access/refresh tokens given the authorization code and stores it to give the app access to user's Google Fit data.
When database is used as storage mode, app uses a PostgreSQL database that contains two main tables managed by SQLModel ORM:
| Column | Type | Constraints | Description |
|---|---|---|---|
user_id |
UUID | PRIMARY KEY | User ID (managed by Supabase Auth) |
entry_date |
DATE | PRIMARY KEY | Date when the weight measurement was taken |
weight |
FLOAT | NOT NULL | Weight value in kilograms |
Composite Primary Key: (user_id, entry_date) - only one weight entry allowed per user per day
Note: No foreign key constraint on user_id because user account management is handled externally by Supabase Auth.
| Column | Type | Constraints | Description |
|---|---|---|---|
user_id |
UUID | PRIMARY KEY | User whose access token we store here (users managed by Supabase Auth) |
token |
VARCHAR | NOT NULL | OAuth access token to access user's Google Fit data |
refresh_token |
VARCHAR | NOT NULL | OAuth refresh token for obtaining new access tokens |
scopes |
VARCHAR | NOT NULL | JSON array of Google API scopes |
token_uri |
VARCHAR | NOT NULL | Google OAuth token endpoint URI |
expiry |
TIMESTAMP | NULLABLE | Access token expiration timestamp |
The project includes best practices to manage code versioning and get it to prod while assuring quality. The diagram below details the developer workflow I used to ship new code to production.
- Tests currently cover only the backend and use
pytestframework. - The test suite includes unit and integration tests. The unit tests are defined by module and the integration tests are split by feature groups.
- Unit tests have a total ~85% coverage with all business logic covered.
- Integration tests use isolated environments to test end-to-end API features (separate test database, mocked Google Fit integration)
Test suite structure:
tests/backend/
├── unit/
│ ├── test_analytics.py
│ ├── test_api.py
│ ├── test_data_integration.py
│ ├── test_db_storage.py
│ ├── test_file_storage.py
│ ├── test_google_fit.py
│ └── test_utils.py
│
└── integration/
├── test_manage_data.py
├── test_get_data.py
└── test_demo_mode.py
All tests:
poetry run pytestUnit tests only:
poetry run pytest tests/backend/unitIntegration tests only:
poetry run pytest tests/backend/integrationThe project requires the following:
-
Docker Desktop - local Supabase (needed for Auth) runs inside Docker container: https://www.docker.com/products/docker-desktop/
-
Supabase CLI - running a local Supabase instance with Auth and PostgreSQL DB if needed: https://supabase.com/docs/guides/local-development/cli/getting-started
-
Python 3.12+ with Poetry package manager
brew install python@3.12 python -m pip install --upgrade pip pip install poetry
-
Node.js 18+ Runtime and npm
brew install node
-
(Optional) Google Cloud Project credentials for Google Fit integration (see below)
-
Clone the repository:
git clone https://github.com/justaszie/weight-tracker.git cd weight-tracker -
Install Python dependencies:
poetry install
-
Start local Supabase:
supabase init supabase start
Important: Save the output values when starting Supabase:
API URLPublishable keyDatabase URL(if you plan to use Supabase PostgreSQL as storage layer)Studio URL:- URL to the admin panel
-
Configure environment: Copy the sample .env file and fill it with values as described in the sample file.
cp ./app/.env.example ./app/.env
-
(Optional) Google Fit Integration setup:
Important: Google plans to deprecate the Google Fit REST APIs in 2026
If you want to test Google Fit integration, you need to set up a Google API client on Google Cloud Platform:
- Create a project in GCP Console
- Enable Fitness API : https://console.cloud.google.com/apis/dashboard
- Create OAuth 2.0 credentials / Client (Web application): https://console.cloud.google.com/apis/credentials
- Open the Client page and add authorized redirect URI:
http://localhost:8000/auth/google-author a different port value depending on the port your FastAPI backend runs on. - Download
credentials.json. Use its values to populate theapp/.envfile. Use the following scopes:
-
Start the backend server:
poetry run uvicorn app.main:app --reload --port 8000 # or other port if 8000 is taken- Backend running at: http://localhost:8000
- API documentation: http://localhost:8000/docs
-
Navigate to frontend directory:
cd ui-react -
Install dependencies:
npm install
-
Configure environment:
Copy the sample .env file and fill it with values as described in the sample file.
cp .env.example .env
-
Start local development server:
npm run dev
Frontend running at: http://localhost:5173
-
Test the App
Go to http://localhost:5173 and sign up for a new account or create a test user in Supabase Studio (Auth section) and log in.
Since learning was the main goal, I wrote all code manually. I did not use AI to generate production code, other than occasional small snippet when using new libraries / APIs. I occasionally used AI tools for debugging support and design feedback.
The initial UI drafts were created with v0 tool but I treated them as visual inspiration and implemented the frontend components and styles (HTML, CSS and React/TS) from scratch.
Interfaces Through Protocols
# Protocol specifying features to be implemented by the storage layer
class DataStorage(Protocol):
def get_weight_entries(self, user_id: UUID) -> list[WeightEntry]: ...
def create_weight_entries(self, entries: Iterable[WeightEntry]) -> None: ...This allows swapping between database and file storage without changing business logic.
# Protocol specifying features to be implemented by the external data source integrations
class DataSourceClient(Protocol):
def get_raw_data(
self, date_from: str | None = None, date_to: str | None = None
) -> Any: ...
def convert_to_daily_entries(self, raw_dataset: Any) -> list[WeightEntry]: ...This allows extending the app functionality with new external data sources with minimal change to existing code.
Factory Functions
# factory function in main.py:
def create_data_storage() -> DataStorage:
storage_type = os.environ.get("STORAGE_TYPE", "database")
match storage_type:
case "database":
return DatabaseStorage()
case "file":
return FileStorage()
case _:
raise ValueError(f"Unsupported storage type {storage_type}")
# Instantiating auth service, storage and logging config as part of app startup
@asynccontextmanager
async def lifespan(app: FastAPI): # type: ignore
data_storage = create_data_storage()
app.state.data_storage = data_storage
logger.info(
f"App started with {data_storage.__class__.__name__} as Storage Backend"
)This allows changing the storage mode of the app from a single point in the app.
Dependency Injection
DataStorageDependency = Annotated[DataStorage, Depends(get_data_storage)]
UserDependency = Annotated[UUID, Depends(get_current_user)]
@router_v1.get("/daily-entries", response_model=list[WeightEntry])
def get_daily_entries(
user_id: UserDependency,
data_storage: DataStorageDependency,
date_from: dt.date | None = None,
date_to: dt.date | None = None,
) -> list[WeightEntry]:Data storage instance and the authenticated user ID are injected automatically into the API routes that use it. We can change the code in a single place (the dependency functions) to make the change in the whole API. Also, these dependencies can be easily mocked in testing.
class DataIntegrationService:
def __init__(
self, data_storage: DataStorage, data_source: DataSourceClient
) -> None:
self.storage = data_storage
self.source = data_sourceData integration logic can work with any data storage implementation and any external data source as these dependencies are injected by the caller.
Strict Type Safety
Catching errors before runtime by implementing clean type annotations wherever possible and running mypy for static type checking pre-commit and before deploying to prod. Note that some libraries like pandas are very dynamic and difficult to annotate specifically.
Error Handling Strategy
def get_data_source_client(
request: Request, source_name: DataSourceName, user_id: UUID
) -> DataSourceClient:
...
oauth_credentials: Credentials | None = GoogleFitAuth().load_credentials(
data_storage, user_id
)
if not oauth_credentials:
raise NoCredentialsError("Google API credentials required")
return GoogleFitClient(user_id, oauth_credentials)
# in /sync-data route logic:
...
except NoCredentialsError:
logger.warning("Google Fit credentials missing or expired")
return JSONResponse(
status_code=401,
content={
"message": "Google Fit authentication needed",
"auth_url": (
f"{http_request.app.url_path_for('google_signin')}?user_id={user_id}"
),
},
)Using specific error types helps the higher-level code provide clearer feedback.
Separation of Concerns
api.py- HTTP layer, params handling, validation, response formattingdata_integration.py- Business logic for data sync. Implements the sync strategyanalytics.py- Pure analytics calculation functionsdb_storage.py- Database operations only- etc.
BEM CSS Methodology
Organizing HTML elements using class names and grouping them into blocks that contain elements and can have various states using modifiers.
/* Block */
.get-data { }
/* Element */
.get-data__cta { }
/* State Modifier */
.get-data__cta--loading { }- Benefits: HTML and CSS are semantic, easy to read and maintain.
- Limitations: Repeated CSS code for similar elements.
Component Composition
Sample composition:
<App>
<Header />
<Main>
<Filters>
<WeeksFilter />
<DatesFilter />
</Filters>
<GetDataSelection />
<NoDataView />
<GetDataSelection />
<Summary />
<WeeklyDataTable />
</Main>
</App>Components have clear responsibilities and are easily reusable. For example, GetDataSelection is used at the top of the main content as well as inside the NoDataView component
Type-Safety
type Goal = 'lose' | 'maintain' | 'gain';
interface SummaryProps {
latestEntry?: WeightEntry | null;
goalSelected: Goal;
weeksFilterValues?: WeeksFilterValues;
datesFilterValues?: DatesFilterValues;
dataUpdated: boolean;
session: Session;
showToast: ShowToastFn;
}- The production app is in Testing state on Google Auth platform. Only users added to the testing group can sync the app with their Google Fit data. I don't plan to complete this verification due to the fact that Google Fit APIs will be deprecated in 2026.
- Backend APIs only support a single frontend domain. To support more diverse clients (e.g. mobile apps, automated scripts), the Google OAuth flow should be changed and the CORS authorized domain management should be made more flexible.
- Frontend and backend are in the same repo/project which adds time to deployments (e.g. Cloudflare pages have to install Python environment in their instance). It's good enough for a small project like this but could be improved.
- MyFitnessPal was implemented as an external data source, alternative to Google Fit API using a 3rd party scraping library. This demonstrates the flexibility of the DataSourceClient protocol. But the library has dependency conflicts with Supabase so the MFP module had to be disabled. I raised a fix PR and am waiting for the merge to fix it.
- The users' Google OAuth access and refresh tokens should be encrypted before storing them in PostgreSQL DB. This would mitigate the risk of the Supabase PostgreSQL DB being breached.
- The frontend React app should be refactored to make it more maintainable.
I’d love to hear from you—whether you’re a hiring manager reviewing this project, a developer interested in collaborating, or someone who would like to share feedback. I am looking for full-stack or backend-focused software engineering roles or contracts.
Justas Zieminykas
- GitHub: justaszie
- Email: justas.zieminykas@gmail.com
- LinkedIn: Justas Zieminykas