- Python
- Django
- React
- TypeScript
- Tailwind CSS
- Azure
- Selenium
- CI/CD
STELLA
Web platform for tracking IVF treatment for patients at Clínica AMARE, with schedule, medications, and a diary.
STELLA is a web application for tracking In Vitro Fertilization (IVF) treatment, built as a team (Group 10 at CESAR School) for patients at Clínica AMARE. The platform centralizes consultations, exams, medications, symptoms, and educational materials from the journey, giving the patient back control over her own treatment. On this page I detail the Django API, where most of the system logic lives, and give a quick overview of the front-end.
Architecture
The monorepo has backend/, frontend/, and e2e/:
- Backend: Python 3.13 + Django 6.0.5 + Django REST Framework. SQLite database in development (the path comes from
DB_PATH) and token authentication (rest_framework.authtoken). Static assets served with Whitenoise and the Django app hosted on an Azure App Service. - Frontend: React 19 + TypeScript + Vite + Tailwind, with TanStack Router (file-based routes with a generated
routeTree.gen.ts) and shadcn/ui components. The screens (calendario,medicacoes,diario,copiloto,exames,videoteca,timeline,perfil) are an SPA and consume the API through a centralapi.ts. Deploy on Azure Static Web Apps. - e2e: 23 tests with Selenium + pytest simulating the user in the browser.
Models
The tratamento Django app concentrates the models:
UserProfile, linked 1:1 toUser, with photo (validated by extension), phone, city, and treatment data (medica_responsavel,protocolo,data_inicio_tratamento).FaseTratamento, the treatment stages withordem_cronologicaand status (pending, current, completed).EventoAgenda, consultations and exams with date, time, and location.Medicacao, with dose, dates, andhorariosin aJSONField(the list of times for each medication).Diario, check-ins withhumor,sintomas, andcondicoes_fisiologicasin aJSONField, plus free-form notes.ConviteCopilotoandVinculoCopiloto, the co-pilot mode: a token link invites a partner/family member with per-area permissions (perm_ver_fase,perm_ver_agenda,perm_ver_medicacoes,perm_receber_lembretes).
A signal ensures every new user is created with a profile:
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
Authentication
Login is its own endpoint (/api/auth/login/) instead of the default Django REST view, because login is by email. It finds the user by email (case-insensitive), authenticates with the password, and returns an authtoken token:
user = User.objects.filter(email__iexact=email).first()
if user is None:
return Response({'detail': 'E-mail ou senha inválidos.'},
status=status.HTTP_401_UNAUTHORIZED)
authenticated_user = authenticate(request, username=user.username, password=password)
if authenticated_user is None:
return Response({'detail': 'E-mail ou senha inválidos.'},
status=status.HTTP_401_UNAUTHORIZED)
token, _ = Token.objects.get_or_create(user=authenticated_user)
return Response({'token': token.key, 'user': { ... }})
Protected views use TokenAuthentication + IsAuthenticated, and the front end sends the token in the header.
The API views
The profile (UserProfileRetrieveUpdateAPIView) returns the user with the nested profile and, on PATCH/PUT, updates both: first the User, then the UserProfile created on demand with get_or_create. The photo has a separate endpoint that requires the file in request.FILES.
The diary is the only resource that is clearly per user: the view filters by the logged-in user’s check-ins and saves with paciente=self.request.user:
class DiarioListCreate(generics.ListCreateAPIView):
serializer_class = DiarioSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Diario.objects.filter(paciente=self.request.user)
def perform_create(self, serializer):
serializer.save(paciente=self.request.user)
The schedule, phases, and medications, on the other hand, use queryset = Model.objects.all() with no authentication and no owner, as a shared global set:
class EventoAgendaListCreate(generics.ListCreateAPIView):
queryset = EventoAgenda.objects.all()
serializer_class = EventoAgendaSerializer
The co-pilot invite returns the token in the response so React can assemble the link (/convite/<token>), and ConviteCopilotoSerializer validates the nome_identificador.
Configuration
settings.py follows the 12-factor pattern: SECRET_KEY, DEBUG, ALLOWED_HOSTS, and CORS come from environment variables with development fallbacks. The database, however, has the engine fixed on SQLite, with only the path changed via DB_PATH:
_db_path = os.environ.get('DB_PATH', str(BASE_DIR / 'db.sqlite3'))
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': _db_path,
}
}
CSRF_TRUSTED_ORIGINS point to the backend and front-end domains on Azure.
Tests, CI/CD, and deploy
There are 32 API tests (Django APITestCase) and 23 E2E tests with Selenium. GitHub Actions runs the suite on every push to main and automates the deploy: the front end goes to Azure Static Web Apps and the backend to Azure App Service.
Particularities and points I would revisit
-
Inconsistent scope and authorization between resources. The diary is filtered by the authenticated user, but
FaseTratamento,EventoAgenda, andMedicacaoexposeobjects.all()with no authentication and no owner. For an individual platform, that data should be per patient; today any request (even without login) lists everything. The contrast between the two approaches is clear in the code and is what I would tackle first. -
Co-pilot invite with a “mock” user.
ConviteCopilotoCreateignores who is logged in and saves the invite forUser.objects.first():def perform_create(self, serializer): usuario_teste = User.objects.first() # Mock user serializer.save(paciente=usuario_teste)It is a development shortcut that stayed in the code: in production the invite would not belong to the right patient. On top of that, the co-pilot permissions (
perm_ver_*) live in the model, but the API has no read endpoints that apply them; the link and the permissions exist, only the fine-grained control per permission does not. -
SQLite in “production” too. The README mentions an Azure database, but the engine is SQLite in any environment, only the file path changes. On a host that recreates the disk on redeploy, the file can be lost; a managed Postgres would avoid that risk.
-
Secrets with a risky fallback.
SECRET_KEYhas a fixed fallback value andDEBUGisTrueby default when the env var is missing. In production the variables are set, but deploying the app without them leaves the server with debug on and a known key. -
Permanent token with no expiration. Login uses
Token.objects.get_or_create, so the same token is valid forever, with no expiration or rotation, and there is no logout endpoint that invalidates it (only deleting it by hand). For an MVP it passes, but a JWT with expiration would be more appropriate. -
Media served by Django itself. The route
re_path(r'^media/...', serve)serves uploads (profile photos) directly through Django; in production it is common to delegate that to external storage. Photo validation also checks only the extension, not the content or the size. -
Orphan dependencies in
requirements.txt.django-ninjaandpydanticare listed, but the code uses Django REST Framework; they probably left over from an earlier attempt.
Improvement points
- Define the owner of each resource (schedule, phases, and medications per patient, or, if they belong to the clinic, make that clear and protect them with staff permission).
- Link the co-pilot invite to the authenticated user and implement the read with the
perm_ver_*for real. - Migrate “production” to Postgres (the
requirements.txtdoes not even have the driver). - Make
SECRET_KEYandDEBUGrequired via env (fail if missing outside dev). - Use JWT with expiration/refresh and a logout endpoint, instead of the fixed token.
- Serve uploads through external storage (blob) and validate the image content and size.
Gabriel Rocha