QR-code-based classroom identification and live timetable system. Deployed and running at school — tracking 53 rooms, 69 teachers, and a full weekly schedule in real time.
Live demo · Report a bug · Request a feature
---- What it does
- Screenshots & pages
- Architecture
- Tech stack
- API reference
- Database schema
- Project structure
- Local development
- Deployment
- How I built this
- AI-assisted development
- Roadmap
In a normal school day you walk a corridor and the only way to know what's happening in a room is to peek through the door. Ticky replaces that with a small piece of paper next to the door: a QR code. You scan it, and within a second you see:
- which class is in there right now
- which teacher is teaching
- what subject
- when the lesson ends
- what comes next The same data also powers a hallway display (a TV in the corridor), a teacher finder (where is teacher X right now?), a class finder (where is class 10.b right now?), and a printable QR sheet for the caretaker to put up new rooms.
The whole thing runs on a free Render dyno + Supabase free tier, costs €0/month, and serves the entire school.
A minimal entry point. Shows the day, current time, system status, and four cards that go to the main views.
All 53 rooms at once, color-coded by status: green = free, red = occupied. Each card shows the current teacher (short code), the class in there, and a thin progress bar for the remaining lesson time. Filters at the top (Összes / Szabad / Foglalt) and a search box.
Pick any of the 69 teachers from a searchable dropdown and the page shows where they are right now (or that they currently have a free hour).
Same idea, but for classes. Grouped by grade (9.→13.), so a student can find their own classroom in two taps.
Prints a QR code per room linking to https://ticky-6r32.onrender.com/terem/{room_number}. Multi-select + “print all” for batch printing.
flowchart LR
user((Student / Teacher)) -- "scans QR" --> phone[📱 Phone browser]
display[📺 Corridor display] -- "auto-refresh 30s" --> render
phone --> render
render["🌐 PHP 8 API<br/>Render.com"]
importer["📥 Node.js Importer<br/>(local, run weekly)"]
excel["📊 timetable.xlsx"]
render -- "SQL via REST" --> supabase["🐘 Supabase<br/>PostgreSQL"]
excel --> importer
importer -- "batch insert" --> supabase
admin((Admin)) -- "secret path" --> render
Two flows:
- Read path (every classroom visitor): browser → PHP API on Render → Supabase REST → PostgreSQL. Sub-200 ms typical response, cached for 30 s.
- Write path (once per week, manual): the school updates an Excel timetable → I run the Node.js importer locally → it wipes the
orarendektable and re-inserts everything in batched transactions. I deliberately kept the write path offline. Schools don't change timetables hourly, and pushing this to a hosted cron job was unnecessary complexity for the actual usage pattern.
| Layer | Tech | Why |
|---|---|---|
| Backend API | PHP 8.x | Zero-config on Render (php -S start command), familiar from school |
| Database | Supabase (PostgreSQL) | Free tier, REST out of the box, auth built in for the admin panel |
| Frontend | Tailwind CSS + Vanilla JS | No build step, fast iteration, easy to deploy |
| Importer | Node.js | xlsx package handles Excel cleanly, easier than PHP for this |
| Hosting | Render.com | Free tier auto-deploys from GitHub |
| Misc | Docker (local dev), Git/GitHub PRs |
All endpoints return JSON, CORS-enabled, no auth required (admin endpoints are scoped under ADMIN_PATH).
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/ping |
Health check |
GET |
/api/termek |
All rooms |
GET |
/api/termek?allapot=1 |
All rooms with live status |
GET |
/api/terem/{number} |
Current & next class for a room |
GET |
/api/napirend/{number} |
Today's schedule for a room |
GET |
/api/napirend/{number}?nap=heten |
Full weekly schedule for a room |
GET |
/api/tanarok |
All teacher codes & names |
GET |
/api/tanar/{code}/orarend |
Today's schedule for a teacher |
GET /api/terem/204{
"terem": "204",
"allapot": "foglalt",
"aktualis": {
"tanar": "ÁSZJ",
"tanar_nev": "Ácsné Szűcs Judit",
"osztaly": "9.b",
"tantargy": "mny",
"kezdes": "09:15",
"vegzes": "10:00",
"perc_maradt": 23
},
"kovetkezo": {
"tanar": "ÁSZJ",
"osztaly": "10.c",
"kezdes": "10:15",
"vegzes": "11:00"
}
}| Table | Fields |
|---|---|
termek |
terem_szam, emelet |
tanarok |
rovid_nev, nev |
orarendek |
terem_id, tanar_id, osztaly, tantargy, het_napja, kezdes, vegzes |
het_napja: 1 = Monday … 5 = Friday
| Period | Start | End |
|---|---|---|
| 1st | 07:30 | 08:10 |
| 2nd | 08:20 | 09:05 |
| 3rd | 09:15 | 10:00 |
| 4th | 10:15 | 11:00 |
| 5th | 11:10 | 11:55 |
| 6th | 12:05 | 12:50 |
| 7th | 12:55 | 13:35 |
| 8th | 13:40 | 14:20 |
ticky/
├── backend/
│ ├── index.php ← Front controller / router
│ ├── config/
│ │ └── supabase.php ← Supabase connection
│ ├── utils/
│ │ └── helpers.php ← Routing, CORS, JSON helpers
│ ├── api/ ← JSON endpoints
│ │ ├── terem.php
│ │ ├── termek.php
│ │ ├── napirend.php
│ │ ├── tanarok.php
│ │ ├── tanar_orarend.php
│ │ ├── admin_tanar.php
│ │ └── admin_terem.php
│ └── pages/ ← HTML pages
│ ├── terem.php ← /terem/{number}
│ ├── termek.php ← /termek
│ ├── napirend.php ← /terem/{number}/nap
│ ├── tanar.php ← /tanar
│ ├── osztaly.php ← /osztaly
│ ├── kijelzo.php ← /kijelzo
│ ├── qr.php ← /qr
│ └── admin.php ← (hidden, ADMIN_PATH)
├── frontend/ ← Tailwind sources, assets
├── importer/ ← Node.js timetable importer
│ ├── importer.js
│ └── package.json
└── adatbazis/ ← SQL schema + seed scripts
- PHP 8.1+ (
php -vto check) - Node.js 18+ (only needed if you want to run the importer)
- A Supabase project (create one free)
# 1. Clone
git clone https://github.com/Davedka/Ticky.git
cd Ticky
# 2. Set up environment
cp .env.example .env
# → fill in SUPABASE_URL, SUPABASE_SERVICE_KEY, ADMIN_PATH
# 3. Create the schema
# Run adatbazis/schema.sql in the Supabase SQL editor
# 4. Start the PHP server
cd backend
php -S localhost:8000 index.phpThen open http://localhost:8000.
cd importer
npm install
node importer.js ../path/to/timetable.xlsxThe importer wipes existing orarendek rows and re-uploads in batched transactions. Idempotent — safe to re-run.
SUPABASE_URL=https://xxxx.supabase.co
SUPABASE_SERVICE_KEY=eyJhbGc... # service_role key, server-side only
ADMIN_PATH=/ticky-panel-7f3a9b2x # any random string — your hidden admin URL- Push to GitHub.
- Render → New Web Service → connect this repo.
- Build command:
echo "No build needed" - Start command:
php -S 0.0.0.0:$PORT backend/index.php - Add the environment variables above.
- Deploy. First boot takes ~1–2 minutes. The free dyno cold-starts in ~10 s after inactivity. For the corridor display, I keep a single uptime ping every 5 minutes so the screen never shows a cold start.
A few decisions worth explaining, because they came up in the thesis defense:
Why Supabase and not just PostgreSQL on Render? Render's free Postgres expires after 90 days. Supabase free is genuinely free indefinitely, and the row-level security + auth I get for the admin panel without writing code is a real win.
Why PHP and not Node/Python?
Render runs a PHP file with php -S as a one-liner start command — no build step, no Dockerfile, no framework setup. For an API this small, anything else would have been over-engineering. I'd probably pick Node + Express for a v2 if I add WebSocket-based live updates.
Why "wipe and re-import" instead of diffing the timetable? The school's timetable changes a few times per semester at most, almost always as a full export from their scheduling software. Diffing two Excel files reliably is harder than re-importing in 4 seconds.
Why a hidden URL instead of a login page for admin?
Because the threat model is "a curious student typing /admin into the URL bar," not a real attacker. The hidden path is paired with Supabase auth — the path just keeps the panel out of search-engine crawls and casual exploration. A login page alone would be more visible without being more secure.
What I'd do differently next time:
- Add WebSocket / SSE updates instead of 30-second polling
- Write the backend in TypeScript with proper typed responses
- Add unit tests for the time-window logic from the start, not at the end
- Use a proper migration tool instead of one big
schema.sql
I used AI tools throughout the project. Being honest about how is more useful than pretending I didn't.
Tools used: [Claude ]
Where they helped:
- Drafting the initial Supabase SQL schema (then reviewed and tightened by hand)
- Generating Tailwind boilerplate for the room cards
- Suggesting the structure for the time-window "what lesson is on now?" function
- Writing this README Where I had to override the AI:
- The first version of the "current lesson" function it suggested didn't handle the gap between lessons correctly (it returned the previous lesson during break time). I caught it by manually testing at 10:05 (during a break) — wrote an explicit test case afterwards.
- It kept suggesting a JS frontend framework (React/Vue) when the project clearly didn't need one. I stuck with vanilla JS + Tailwind on purpose. My takeaway: AI is a fast pair-programmer that doesn't get tired of boilerplate, but it confidently produces wrong code at the edges (time math, off-by-one errors, auth flow corners). Trust nothing without running it.
- Unit tests for the time-window logic (PHPUnit)
- GitHub Actions CI: run tests on every push
- Server-Sent Events for live updates instead of polling
- Per-teacher weekly schedule view (currently only daily)
- Lyukasóra notifier (notify when a class has a free hour mid-day)
- Dark/light theme toggle
- i18n — English version of the UI for international visitors




