A production-ready REST API for managing user subscriptions, with JWT authentication, automated reminder emails scheduled via background workers, and per-route rate limiting. Built in Python with FastAPI.
Live API: https://web-production-bba32.up.railway.app/
Interactive docs (Swagger UI): https://web-production-bba32.up.railway.app/docs
This project is a Python reimplementation of Adrian Hajdin's subscription-tracker-api, which was originally built in JavaScript with Express, MongoDB, and more.
A signed-up user can create subscriptions (Netflix, Spotify, gym membership, etc.) with a name, price, frequency, and start date. The app automatically computes when the subscription will renew based on this info, and sends email reminders a few days before.
The reminders aren't sent during the API request that creates the subscription; that would be impossible (the request finishes in milliseconds, but the reminder isn't due for days). Instead, the API enqueues a background job to send a reminder email at the right time.
| Layer | Technology | Purpose |
|---|---|---|
| Web framework | FastAPI | Async HTTP routing, request/response validation, OpenAPI |
| ASGI server | Uvicorn (workers managed by Gunicorn) | Production process management |
| MongoDB ODM | Beanie (built on Motor) | Async typed document models with lifecycle hooks |
| Validation | Pydantic v2 | Request/response schemas, settings management |
| Auth | python-jose + passlib (bcrypt) | JWT signing/verification, password hashing |
| Rate limiting | SlowAPI | Per-IP token-bucket limiting on auth routes |
| Background jobs | Celery + Redis | Scheduled email reminders |
| FastAPI-Mail | Async SMTP via Gmail | |
| Database | MongoDB Atlas | Hosted document database |
| Hosting | Railway | Deployment for web + worker + Redis |
| Testing | pytest + pytest-asyncio | Integration tests for model hooks |
The application is composed of three independent services that communicate through MongoDB and Redis:
ββββββββββββββββ HTTP βββββββββββββββββββ
β Client β βββββββββββββΊ β FastAPI (web) β
β (browser, β β - Gunicorn β
β curl, etc) β βββββββββββββ β - 2 workers β
ββββββββββββββββ JSON ββββββββββ¬βββββββββ
β
β (1) insert
βΌ
βββββββββββββββββββ
β MongoDB Atlas β
ββββββββββ¬βββββββββ
β
β (2) read on read endpoints
βΌ
βββββββββββββββββββ
β FastAPI (web) β
ββββββββββ¬βββββββββ
β
β (3) enqueue job
βΌ
βββββββββββββββββββ
β Redis β
β (broker) β
ββββββββββ¬βββββββββ
β
β (4) poll, get task
βΌ
βββββββββββββββββββ
β Celery worker β SMTP
β - 2 concurrency β ββββββββββΊ Gmail
β - schedules & β
β sends emails β
βββββββββββββββββββ
The web service handles HTTP requests and finishes them quickly. It can't be the one that "waits 7 days then sends an email" β there'd be nothing waiting; FastAPI requests don't persist that long. The Celery worker handles that.
Subscription-Management-System/
βββ app.py # FastAPI factory, lifespan, middleware, router registration
βββ Procfile # Railway: web + worker process definitions
βββ runtime.txt # Pinned Python version for deployment
βββ requirements.txt # All pinned dependencies
βββ pytest.ini # pytest-asyncio config
β
βββ config/
β βββ env.py # Typed pydantic-settings singleton
β βββ database.py # Motor client + Beanie init
β βββ mail.py # FastAPI-Mail ConnectionConfig
β βββ celery_app.py # Celery app instance
β
βββ models/
β βββ user.py # Beanie Document: User
β βββ subscription.py # Beanie Document: Subscription + lifecycle hooks
β
βββ schemas/
β βββ auth.py # SignUpRequest, SignInRequest, AuthResponse
β βββ user.py # UserPublic (password-free)
β βββ subscription.py # SubscriptionCreate, SubscriptionResponse
β
βββ routers/
β βββ auth.py # /api/v1/auth (sign-up, sign-in, sign-out)
β βββ users.py # /api/v1/users
β βββ subscriptions.py # /api/v1/subscriptions
β βββ workflows.py # /api/v1/workflows (manual reminder trigger)
β
βββ controllers/
β βββ auth.py # sign_up, sign_in, sign_out
β βββ users.py # get_users, get_user
β βββ subscriptions.py # full CRUD + cancel + upcoming-renewals
β βββ workflows.py # send_reminders
β
βββ middleware/
β βββ auth.py # FastAPI Depends: JWT Bearer β current_user
β βββ rate_limit.py # SlowAPI Limiter
β βββ error_handler.py # Exception handlers (400/401/404/422/500)
β
βββ tasks/
β βββ reminder.py # Celery: schedule_reminders, send_reminder_email_task
β
βββ utils/
β βββ jwt.py # JWT encoding + expiry-string parser
β βββ send_email.py # Async send_reminder_email
β βββ email_templates.py # 7/5/2/1-day reminder templates
β
βββ tests/
βββ conftest.py # Test DB fixtures
βββ test_models/
β βββ test_subscription.py # Lifecycle hook tests
β βββ test_users.py # User interaction tests
βββ test_api/
β βββ test_auth.py # Authentification tests
| βββ test_subscriptions.py # Subscriptions tests
| βββ tests_users.py # Users tests
βββ test_schemas/
βββ test_auth_schemas.py # Authentification schemas tests
βββ test_subscriptions_schemas.py # Subscriptions schemas tests
All endpoints are prefixed with /api/v1. Full interactive documentation is available at /docs on the live deployment.
| Method | Path | Description | Auth | Rate limit |
|---|---|---|---|---|
| POST | /sign-up |
Create user, return JWT | β | 5 / 10 seconds / IP |
| POST | /sign-in |
Verify password, return JWT | β | 5 / 10 seconds / IP |
| POST | /sign-out |
Stub (token discarded client-side) | β | β |
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | / |
List all users | β |
| GET | /{user_id} |
Get one user (no password) | β |
| POST | / |
501 Not Implemented | β |
| PUT | /{user_id} |
501 Not Implemented | β |
| DELETE | /{user_id} |
501 Not Implemented | β |
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /upcoming-renewals |
Active subs renewing within 7 days | β |
| GET | /user/{user_id} |
Subs for a specific user (ownership check enforced) | β |
| GET | / |
List all subscriptions | β |
| GET | /{sub_id} |
Get one subscription | β |
| POST | / |
Create subscription, enqueue reminder schedule | β |
| PUT | /{sub_id}/cancel |
Set status to "cancelled" | β |
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /subscription/reminder |
Manually re-enqueue reminders for a sub | β |
Swagger UI auto-generated from FastAPI's type annotations and Pydantic schemas.

- Python 3.12
- A MongoDB Atlas account (free tier is fine)
- Redis (run via Docker, native install, or a managed provider)
- A Gmail account with an app password generated for SMTP
# Clone and enter the project
git clone https://github.com/FirstOne96/Subscription-tracker.git
cd Subscription-tracker
# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # on Windows: .venv\Scripts\activate
# Install dependencies
pip install -r requirements.txtCreate a .env.development.local at the project root (it's gitignored β never commit secrets):
PORT=8000
MONGODB_URI=mongodb+srv://<user>:<password>@<cluster>.mongodb.net/subscription_db?retryWrites=true&w=majority
JWT_SECRET=<generate with: python -c "import secrets; print(secrets.token_urlsafe(64))">
JWT_EXPIRES_IN=1d
REDIS_URL=redis://localhost:6379/0
MAIL_USERNAME=your_email@gmail.com
MAIL_PASSWORD=<your 16-character Gmail app password, no spaces>
MAIL_FROM=your_email@gmail.com
MAIL_PORT=587
MAIL_SERVER=smtp.gmail.com
MAIL_STARTTLS=True
MAIL_SSL_TLS=False
ALLOWED_ORIGINS=
If you have Docker:
docker run -d --name redis -p 6379:6379 --restart unless-stopped redisVerify it's reachable: redis-cli ping should return PONG.
You'll need three things running. Open three terminals (or one with tmux/split panes):
Terminal 1 β Redis (already running in the background if you used --restart unless-stopped above)
Terminal 2 β FastAPI:
uvicorn app:app --reload --port 8000Terminal 3 β Celery worker:
celery -A config.celery_app worker --loglevel=info --concurrency=2Visit http://localhost:8000/docs to interact with the API.
pytest tests/ -vThe tests run against a separate subscription_db_test database in your Atlas cluster. The database is dropped at the end of each session.
The live deployment is on Railway, with three services:
- Web service β runs
gunicorn app:app -w 2 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:$PORT - Worker service β runs
celery -A config.celery_app worker --loglevel=info --concurrency=2 - Redis service β Railway-managed, referenced via
${{ Redis.REDIS_URL }}
Both web and worker services pull from the same GitHub repository, with different start commands. They share the same environment variables (set independently on each service in Railway's dashboard, securely).
MongoDB Atlas hosts the production database (subscription_db_prod), with network access opened to 0.0.0.0/0 (still gated by the database password).
Three services running on Railway: web (FastAPI + Gunicorn), worker (Celery), and Redis (broker).

Two production databases: subscription_db_prod (deployed) and subscription_db (local development).

The original JavaScript version of this project (Express + Mongoose + Upstash QStash + Arcjet + Nodemailer) is at https://github.com/adrianhajdin/subscription-tracker-api.
The companion video tutorial is at https://www.youtube.com/watch?v=rOpEN1JDaD0.
Andrii Kozlov - andrijkozlov96@gmail.com | https://t.me/AndrewKozz | https://www.linkedin.com/in/andrii