The problem
Millions of people work rotating shifts — nurses, security guards, factory workers, firefighters. Most of them organize their schedules on paper, WhatsApp groups, or spreadsheets. Every month, the same question in the group chat: "Am I working Saturday?"
I decided to build something better.
What I built
Turna is a free shift calendar app designed specifically for shift workers. You can:
- 🎨 Paint shifts on a calendar with custom colors
- 🔄 Generate rotating sequences automatically (e.g., Morning → Afternoon → Night → Off → Off)
- 👥 Share your calendar with your partner or coworkers in real time
- 📊 Track statistics — hours worked, days off, breakdown by shift type
- 🌙 Use dark mode for night shifts
It's available as a web app at app.turna.es and on Google Play as a TWA.
👉 Landing page: turna.es
The tech stack
I wanted to keep things simple and fast:
| Layer | Technology |
|---|---|
| Frontend | Vue.js 3 with Composition API, pure CSS (no frameworks) |
| Backend | Python + FastAPI |
| Database | PostgreSQL |
| Auth | FastAPI Users with JWT tokens |
| Hosting | Linux VPS with Nginx |
| Mobile | PWA + TWA built with Bubblewrap |
No React, no Next.js, no Tailwind, no Docker — just the basics, done well. The entire frontend is a single HTML file + a JS file. Total bundle size: tiny.
Key decisions
PWA + TWA instead of native apps. I'm one person. Building native apps for iOS and Android would take months. Instead, Turna is a PWA (installable from the browser) wrapped as a TWA for Google Play using Bubblewrap. One codebase, available everywhere.
Pure CSS instead of a framework. Bootstrap and Tailwind are great, but for a calendar app where every pixel matters, I wanted full control. The result: a fast, lightweight UI that feels native on mobile.
FastAPI for the backend. Python is what I know best, and FastAPI is incredibly productive for building REST APIs. Auto-generated docs, async support, and type validation out of the box.
Paint mode. This was the feature that changed everything. Instead of filling forms to add shifts, you just select a shift type and tap days on the calendar. Like painting. Users can set up an entire month in 10 seconds.
The hardest parts
Shared calendars with diagonal view. When two people share calendars, their shifts need to appear overlaid on the same day cells. I implemented a CSS diagonal split — the top-left triangle shows one person's shift, the bottom-right shows the other. Simple idea, tricky implementation.
Timezone handling for statistics. Shifts stored as dates in the database, but JavaScript Date objects converting to UTC when calling the API. Night shifts on the last day of the month would "disappear" from statistics because UTC conversion pushed them to the next month. Fixed by ensuring the query range always includes 23:59:59 of the last day.
TWA splash screen. Getting the splash screen to look right on Android required matching background_color in both the web manifest and the TWA manifest, plus properly formatted maskable icons. Lots of trial and error.
Current status
- ✅ Web app live at app.turna.es
- ✅ Android app in closed testing on Google Play
- ✅ 12+ testers
- ✅ Landing page at turna.es
- 🔜 Public release on Google Play
- 🔜 Premium features (Google Calendar export, push notifications)
What I learned
- Ship early, iterate fast. My first version was ugly. Users didn't care — they cared that it worked.
- Build for a specific audience. "Calendar app" is too broad. "Shift calendar for nurses" is a niche I can win.
- A solo developer CAN ship a full product. Frontend, backend, database, PWA, TWA, landing page, Play Store listing — it's a lot, but it's doable if you keep the stack simple.
Try it
- 🌐 Website: turna.es
- 📱 App: app.turna.es
- 💻 GitHub: github.com/jlodev-desing/turna
- 📸 Instagram: @jlodev.desing
If you work shifts (or know someone who does), give it a try and let me know what you think! Feedback is always welcome.
Built by JLODev — Web development & apps.
Top comments (0)