Prove the right person, in the right room, on the right device, inside the warden's window - with no new hardware.
Team Name: Zero_Latency · Team ID: GEN26-A0H-H051
- The problem
- The solution
- Tech stack
- System architecture
- Verification pipeline
- One night, end to end
- Enrollment
- Trust score
- Geofencing with WiFi
- Threat model
- Data model
- Deployment
- Privacy
- Demo plan
- Roadmap
- Getting started
- Repository layout
- Project status
- License
Every night at 10:00 PM, every hosteller has to walk down to the ground floor to prove they are in the hostel. Students on the first, second, third and higher floors all queue at the same scanner, whatever their situation.
- A student with a fever or a fall-related injury still has to climb down the stairs to be marked. Some stay in bed and get marked absent instead.
- A final-year student back from an interview, or in the middle of a video call with a recruiter, loses time and focus on the walk down and back up.
- Students who are unwell or hurt are the ones most likely to need help, but the system gives the warden no sign of who is in their room.
- The check proves you were at the ground floor at 10:00 PM. It says nothing about whether you are safe, or whether you stayed.
NightRoll moves the check to the student's phone and the hostel's existing per-room WiFi. At 10 PM the student opens NightRoll and completes a short face check in bed, and the room tile turns green on the warden's dashboard.
A selfie alone would be easy to fake, so every mark has to prove three things at once:
| Proof | How | What it stops |
|---|---|---|
| Right person | Face match plus a random liveness challenge | Someone else marking for you, photos, replayed video |
| Right place | Request arrives from your room's subnet, backed by a WiFi fingerprint of the room | Marking from outside the hostel, VPN tunnelling |
| Right device, right now | A hardware-bound key signs a one-time nonce issued for tonight's window | Marking from a friend's phone, replaying yesterday's request |
No single factor is trusted on its own. The existing ground-floor scanner stays as the fallback.
flowchart LR
subgraph Before["Today"]
direction LR
A1["Floors 1 to N"] --> A2["Walk down"] --> A3["Queue at one scanner"] --> A4["Walk back up"]
end
subgraph After["With NightRoll"]
direction LR
B1["Opens app at 22:00"] --> B2["Face check in bed"] --> B3["Marked present"]
end
Before -.-> After
Mobile app
Backend
Face and liveness
Data
Dashboard and delivery
| Layer | Choice | Why |
|---|---|---|
| App | Expo (React Native, TypeScript), Expo Router, Kotlin Expo Modules, VisionCamera, ML Kit | Expo for builds and EAS cloud builds; a development build runs the native code; one codebase for screens |
| On-device ML | ML Kit face detection; TFLite MobileFaceNet planned | Quick pre-check so a bad capture fails before upload |
| Device trust | Android Keystore / StrongBox, Play Integrity | Key never leaves the chip; blocks root, emulators, virtual cameras |
| API | FastAPI, Pydantic, Uvicorn | Async, typed, and every request is validated against a schema; fast to build |
| State | Upstash Redis (hosted, TLS) | Nonces, window state and rate limits with native TTL, no local server to run |
| Face | InsightFace buffalo_l on ONNX Runtime, Silent-Face anti-spoofing |
Strong open ArcFace embeddings and an anti-spoof check, both on CPU |
| Database | Supabase (PostgreSQL with pgvector), asyncpg | Relational data and 512-dimension embeddings in one managed database |
| Auth | Supabase Auth | Warden and admin login with MFA; student devices are bound by Keystore keys, not passwords |
| Frame storage | Supabase Storage (private bucket) | Encrypted, short-lived frames with a 24-hour deletion job |
| Network verify | Subnet check built; controller API or RADIUS accounting behind one interface | Starts simple, upgrades without app changes |
| Dashboard | React, Vite, React Router, TanStack Query over WebSocket | Room tiles update live, and fall back to polling when the socket drops |
| Window status | Pulled from the API when the app opens | No push service; students open the app at window time |
Four zones: the phone gathers evidence, the hostel network vouches for location, the backend decides, and the outputs inform the student and the warden.
flowchart LR
subgraph Phone["1. Student phone"]
direction TB
P1["Guard checks<br/>VPN, proxy, mock location"]
P2["Liveness challenge"]
P3["Face frames"]
P4["WiFi scan"]
P5["Play Integrity token"]
P6["Keystore signer"]
P7["Signed payload"]
P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7
end
subgraph Net["2. Hostel network"]
direction TB
N1["Room access point"]
N2["Room VLAN / subnet"]
N3["WiFi controller / RADIUS"]
N1 --> N2
N1 -.-> N3
end
subgraph Back["3. NightRoll backend on campus LAN"]
direction TB
B0["Window service"]
B1["API gateway"]
B2["Hard gates"]
B3["Scored checks"]
B4["Fusion engine"]
B5[("Supabase PostgreSQL + pgvector")]
B6[("Upstash Redis")]
B7["Network adapter"]
B0 --> B1 --> B2 --> B3 --> B4
B0 <--> B6
B4 --> B5
B7 --> B3
end
subgraph Out["4. Outputs"]
direction TB
O1["Window status on Home"]
O2["Student result"]
O3["Warden dashboard"]
end
P7 -- "HTTPS" --> N1
N2 -- "source IP = room" --> B1
N3 -.-> B7
B0 --> O1
O1 -- "shown when app opens" --> P1
B4 --> O2
B4 --> O3
flowchart TB
GW["API gateway"] --> ORC["Verification orchestrator"]
ORC --> CV["Crypto verifier<br/>signature, nonce freshness"]
ORC --> IV["Integrity verifier<br/>Play Integrity decode"]
ORC --> NV["Network verifier<br/>subnet, controller, RADIUS"]
ORC --> FV["Face verifier<br/>ArcFace, anti-spoof"]
ORC --> WV["Fingerprint verifier<br/>room WiFi pattern"]
CV --> FE["Fusion engine"]
IV --> FE
NV --> FE
FV --> FE
WV --> FE
WS["Window service"] --> RD[("Upstash Redis<br/>nonces, window state")]
CV --> RD
FV --> PG[("Supabase PostgreSQL + pgvector<br/>students, rooms, embeddings, logs")]
WV --> PG
FE --> PG
FE --> RT["WebSocket hub"] --> DB["Warden dashboard"]
Hard gates run first. Any single failure rejects the mark before a face is ever compared. Only marks that pass every gate are scored.
flowchart TD
S(["Signed payload arrives"]) --> G1{"Inside window?<br/>server clock"}
G1 -- no --> R["REJECTED"]
G1 -- yes --> G2{"Signature valid<br/>and nonce unused?"}
G2 -- no --> R
G2 -- yes --> G3{"Play Integrity pass?"}
G3 -- no --> R
G3 -- yes --> G4{"No VPN, proxy<br/>or mock location?"}
G4 -- no --> R
G4 -- yes --> G5{"Source IP in<br/>room subnet?"}
G5 -- no --> R
G5 -- yes --> SC["Score: face, liveness,<br/>room fingerprint, RSSI"]
SC --> F{"Trust score"}
F -- "0.80 or above" --> P["PRESENT"]
F -- "0.60 to 0.80" --> V["NEEDS REVIEW"]
F -- "below 0.60" --> R
V --> W["Warden review queue"]
R --> FB["Fallback: ground-floor scanner"]
| Gate | Defeats |
|---|---|
| Server-clock window | Changing the phone's time |
| Signature and fresh nonce | A friend's phone, replayed requests |
| Play Integrity | Rooted devices, emulators, virtual cameras |
| No VPN, proxy or mock location | Tunnelling in from outside |
| Room subnet source IP | Any request from off the hostel network |
sequenceDiagram
autonumber
actor W as Warden
participant D as Dashboard
participant S as Backend
participant R as Upstash Redis
participant A as NightRoll app
actor U as Student
participant AP as Room AP
W->>D: Set window, default 22:00 to 22:30
D->>S: Save window and off days
Note over S: 22:00 window opens
S->>R: Store one nonce per student with TTL
U->>A: Opens NightRoll in bed
A->>A: Guard checks for VPN, proxy, mock location
A->>U: Random liveness challenge
U->>A: Blink, turn head
A->>A: Capture frames, scan WiFi, fetch integrity token
A->>A: Sign payload with Keystore key
A->>AP: POST verify over HTTPS
AP->>S: Forward, source IP is the room subnet
S->>R: Check and consume nonce
S->>S: Hard gates, then scoring, then fusion
S-->>A: Verdict with reason
S-->>D: Room tile updates live
Note over S: 22:30 window closes
S->>R: Nonces expire
S-->>D: Absentee list and review queue
W->>D: Review flagged marks, override if needed
stateDiagram-v2
[*] --> Pending: window opens
Pending --> Present: gates pass and score is high
Pending --> NeedsReview: gates pass and score is borderline
Pending --> Rejected: a gate fails or score is low
Pending --> Absent: window closes with no mark
NeedsReview --> Present: warden approves
NeedsReview --> Absent: warden declines
Rejected --> Pending: student retries inside the window
Rejected --> Present: ground-floor scanner or warden override
Absent --> Present: warden override
Present --> [*]
Absent --> [*]
Enrollment happens once, in person, at the warden's office. That is what makes the later remote checks trustworthy.
flowchart TD
A(["Student at warden office"]) --> B["Consent recorded"]
B --> C["Controlled face capture"]
C --> D["ArcFace embedding, 512 dimensions"]
D --> E[("Store vector in Supabase pgvector<br/>photo discarded")]
B --> F["App generates keypair in Keystore"]
F --> G[("Register public key to student")]
B --> H["3 to 5 WiFi scans in the room<br/>bed, desk, door"]
H --> I[("Room fingerprint")]
J["Network admin"] --> K[("Room to BSSID to subnet map")]
E --> L(["Enrolled"])
G --> L
I --> L
K --> L
- Face: only the embedding is stored. The enrollment photo is discarded.
- Device: the private key is generated inside the phone's secure hardware and never leaves it. A new phone means re-enrolling the device.
- Room: one student scans the room; roommates share the same room fingerprint.
Marks that pass every gate are scored from five signals.
pie showData
title Trust score weights (provisional)
"Face match" : 30
"Room WiFi fingerprint" : 30
"Liveness and anti-spoof" : 20
"Own-AP signal strength" : 15
"AP-side signal agreement" : 5
| Signal | Weight | What it measures |
|---|---|---|
| Face match | 0.30 | Cosine similarity between tonight's embedding and the enrolled one |
| Room WiFi fingerprint | 0.30 | How closely tonight's scan matches the room's saved pattern |
| Liveness and anti-spoof | 0.20 | Challenge completed, and the face is not a photo or a screen |
| Own-AP signal strength | 0.15 | The room's own router is seen at room-level strength |
| AP-side signal agreement | 0.05 | The router's view of the phone agrees with the phone's report |
| Score | Verdict | What happens |
|---|---|---|
| 0.80 or above | PRESENT | Recorded; room tile turns green |
| 0.60 to 0.80 | NEEDS REVIEW | Goes to the warden's review queue |
| Below 0.60 | REJECTED | Student sees the reason and can retry or use the ground-floor scanner |
Note
These weights and cutoffs are starting values. They will be tuned against real measurements from the hostel (see the walk test below) before anyone relies on them.
GPS cannot tell one room from the next indoors, and it is easy to fake. NightRoll uses the WiFi itself as the fence.
Every spot in the hostel sees the surrounding routers differently. Inside room 203, the room's own router is very strong, the neighbours are medium, the corridor is weak. Outside the compound wall, most hostel routers vanish and unfamiliar networks appear. That pattern is the room's fingerprint.
flowchart TD
A(["Tonight's WiFi scan"]) --> B{"Own room router<br/>present and strong?"}
B -- no --> X["Low fingerprint score"]
B -- yes --> C{"Expected neighbour<br/>routers present?"}
C -- "few" --> X
C -- "most" --> D{"Relative strengths<br/>match the usual order?"}
D -- no --> Y["Medium fingerprint score"]
D -- yes --> E{"Unknown outside<br/>networks visible?"}
E -- "several" --> Y
E -- "none or few" --> Z["High fingerprint score"]
X --> F(["Feeds trust score"])
Y --> F
Z --> F
Limits are measured, not guessed.
flowchart LR
A["Scan inside<br/>several rooms"] --> C["Compare the<br/>two groups"]
B["Scan outside:<br/>compound wall, corridor,<br/>next room, floor below"] --> C
C --> D["Set limits<br/>between them"]
D --> E["Stricter limits for<br/>rooms near the wall"]
- Slow drift: confident "present" marks are blended into the saved fingerprint. Borderline marks never are, so a cheater cannot teach the system a wrong pattern.
- Router replaced: the warden triggers a re-scan for that room.
- Stale fingerprint: repeated reviews from one room flag it on the dashboard.
| Layer | Role |
|---|---|
| Router minimum-signal setting | The router drops weak clients, so distant phones cannot stay connected |
| Router-side signal check | A large gap between the router's reading and the phone's report suggests a faked scan |
| 5 GHz signal | Blocked by walls more than 2.4 GHz; used as evidence if the walk test shows it helps, not as a rule |
| GPS | A warning only, and only when accurate and clearly outside the hostel |
flowchart LR
T1["Friend marks for me<br/>on their phone"] --> D1["Device key signature"]
T2["Friend marks for me<br/>on my phone"] --> D2["Face match"]
T3["Photo or video<br/>of my face"] --> D3["Liveness and anti-spoof"]
T4["Replay last<br/>night's request"] --> D4["One-time nonce"]
T5["Mark from outside<br/>over VPN"] --> D5["VPN guard and<br/>room subnet gate"]
T6["Catch weak WiFi from<br/>outside the wall"] --> D6["Room fingerprint<br/>and signal strength"]
T7["Rooted phone, emulator<br/>or fake camera"] --> D7["Play Integrity"]
T8["Change the<br/>phone clock"] --> D8["Server-clock window"]
| Limitation | Handling |
|---|---|
| Student marks present, then leaves | Out of scope for the first release; random spot-checks are on the roadmap |
| Phone dead, no WiFi, router down | Ground-floor scanner stays as the fallback |
| iPhones cannot do full WiFi scans | Android first; iPhone users keep the ground-floor scanner |
| Hostel network does not expose subnets or a controller API | Needs the network admin; the design degrades to fingerprint plus review |
erDiagram
STUDENT ||--o| DEVICE : registers
STUDENT ||--|| FACE_EMBEDDING : has
STUDENT }o--|| ROOM : lives_in
ROOM ||--o{ ACCESS_POINT : served_by
ROOM ||--o| ROOM_FINGERPRINT : has
ATTENDANCE_WINDOW ||--o{ NONCE : issues
STUDENT ||--o{ NONCE : receives
STUDENT ||--o{ ATTENDANCE_MARK : makes
ATTENDANCE_WINDOW ||--o{ ATTENDANCE_MARK : contains
ATTENDANCE_MARK ||--o| REVIEW : may_need
STUDENT {
uuid id PK
string roll_number
string name
uuid room_id FK
timestamp consent_at
}
DEVICE {
uuid id PK
uuid student_id FK
text public_key
timestamp enrolled_at
}
FACE_EMBEDDING {
uuid student_id PK
vector embedding
}
ROOM {
uuid id PK
string number
int floor
string subnet
}
ACCESS_POINT {
string bssid PK
uuid room_id FK
string band
}
ROOM_FINGERPRINT {
uuid room_id PK
json pattern
timestamp updated_at
}
ATTENDANCE_WINDOW {
uuid id PK
date night
timestamp opens_at
timestamp closes_at
}
NONCE {
string value PK
uuid student_id FK
uuid window_id FK
boolean used
}
ATTENDANCE_MARK {
uuid id PK
uuid student_id FK
uuid window_id FK
float trust_score
string verdict
string reason
timestamp marked_at
}
REVIEW {
uuid mark_id PK
string decision
string reviewed_by
timestamp reviewed_at
}
Nonces live in Upstash Redis with a TTL that ends when the window closes. They are shown here for the relationships only.
The backend sits on the campus LAN so the source IP of every request is the student's real room address.
flowchart TB
subgraph Rooms["Hostel rooms"]
PH["Student phones"]
AP["Room access points"]
PH --> AP
end
subgraph LAN["Campus LAN"]
CTRL["WiFi controller / RADIUS"]
subgraph Host["NightRoll host"]
API["FastAPI service"]
WK["Face worker"]
RDS[("Upstash Redis")]
PGS[("Supabase PostgreSQL + pgvector")]
WEB["Dashboard"]
API --> RDS
API --> PGS
API --> WK
WK --> PGS
end
WD["Warden desk"]
end
subgraph Cloud["Google services"]
PIS["Play Integrity API"]
end
AP --> API
CTRL -.-> API
WD --> WEB
WEB <--> API
API --> PIS
Face data is sensitive, so the rules are fixed from the start.
- Consent first. Enrollment requires explicit, recorded consent.
- Embeddings, not photos. Only the numeric face embedding is stored.
- Short-lived frames. Frames captured for a nightly check are deleted within 24 hours.
- No biometric logging. Embeddings and raw frames never appear in logs.
- Reasons shown. A student always sees why a mark was rejected or sent to review.
- A human decides the edge cases. The warden can override any automatic verdict.
Two ordinary routers stand in for Room 101 and Room 102, each on its own subnet.
| # | Action | Expected result |
|---|---|---|
| 1 | Mark from the Room 101 router | PRESENT; the tile turns green |
| 2 | Switch to the Room 102 router | REJECTED at the room subnet gate |
| 3 | Hold up a photo or a video of the face | REJECTED by liveness |
| 4 | Mark from a teammate's phone | REJECTED; signature does not match |
| 5 | Walk out of the building while still on WiFi | REJECTED; WiFi pattern does not match the room |
Production plugs into the hostel's existing WiFi controller or RADIUS. No new hardware is required.
flowchart LR
subgraph M1["MVP: hackathon"]
direction TB
a1["Signed nonce per student"]
a2["Room subnet gate"]
a3["Face match and liveness challenge"]
a4["Dashboard with room tiles"]
end
subgraph M2["Next"]
direction TB
b1["WiFi fingerprint and walk test"]
b2["Passive anti-spoof model"]
b3["Controller or RADIUS adapter"]
end
subgraph M3["Later"]
direction TB
c1["Random spot-checks"]
c2["Anomaly detection"]
c3["ERP export, iOS support"]
end
M1 --> M2 --> M3
Install these once. The commands are for macOS with Homebrew; Windows and Linux users can use the official installers.
| Tool | Version | Needed for | Install |
|---|---|---|---|
| Git | any recent | Source control | brew install git |
| uv | current | Backend and face worker: Python versions, dependencies | brew install uv |
| Python | 3.12 | Backend, face worker (uv installs it, so this is optional) | brew install python@3.12 |
| Node.js | 20 LTS | Dashboard, app tooling | brew install node@20 |
| JDK | 17 | Android build | brew install openjdk@17 |
| Android Studio | current stable | Android SDK, emulator | developer.android.com/studio |
| Docker Desktop | current stable | Later services (Phase 12); not needed for Redis | docker.com, then start it |
| EAS CLI | current | Development and production builds of the app | npx eas-cli (no install needed) |
| Supabase CLI | current stable | Migrations, linking the project | brew install supabase/tap/supabase |
-
Clone the repo and switch to the branch you are working on:
git clone https://github.com/Rahul-8283/NightRoll.git cd NightRoll git checkout base -
Create your local env files from the examples. Ask the team for the real values; never commit them.
cp backend/.env.example backend/.env cp face/.env.example face/.env cp dashboard/.env.example dashboard/.env cp app/.env.example app/.env cp config/rooms.example.json config/rooms.local.json
-
Put the Google service account file at
backend/secrets/google-service-account.json(from the team password manager). -
Paste your Upstash
rediss://URL intobackend/.envasREDIS_URL, then check it (see below). -
Link Supabase: follow supabase/README.md.
| Service | Port | Start with | Available from |
|---|---|---|---|
| Upstash Redis | hosted, TLS | set REDIS_URL in backend/.env |
Phase 0 (now) |
| Supabase | hosted | supabase link --project-ref <ref> |
Phase 0 (now) |
| Backend API | 8000 | cd backend && uv run uvicorn app.main:app --reload --port 8000 |
Health route now; full API in Phase 2 |
| Window scheduler | none | cd backend && python -m app.scheduler |
Phase 3 |
| Face worker | 8100 | cd face && uvicorn worker.main:app --port 8100 |
Phase 5 |
| Warden dashboard | 5173 | cd dashboard && npm run dev |
Shell now; screens in Phase 10 |
| Student app | Metro 8081 | cd app && npx expo start |
Shell now; features in Phases 4 to 7 (Expo Go for screens; development build for the rest) |
Start order once everything exists: face worker, backend API, scheduler, dashboard, then the phone app. Upstash Redis is always on.
Redis (works now, hosted on Upstash)
Redis runs in the cloud, so there is no server to start. Check the connection with the Redis client in a container, so nothing needs installing on the laptop:
docker run --rm redis:7 redis-cli --tls -u "<your REDIS_URL>" ping # expect PONGUse the same rediss:// URL as in backend/.env. Do not commit it.
Backend API (set up now with uv)
cd backend
uv sync
uv run uvicorn app.main:app --reload --port 8000
curl http://localhost:8000/v1/health # expect {"status":"ok"}Window scheduler (from Phase 3) opens and closes attendance windows at the right time, and records housekeeping tasks. Run it in its own terminal:
cd backend
uv run python -m app.schedulerThe scheduler writes a heartbeat to Redis every 15 seconds. If the heartbeat goes stale (older than 60 seconds), the dashboard shows a critical alert. To restart a stalled scheduler:
# In Docker
docker compose -f infra/docker-compose.yml restart scheduler
# Or locally
cd backend && uv run python -m app.schedulerFace worker (from Phase 5)
cd face
python3.12 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
python scripts/download_models.py
uvicorn worker.main:app --port 8100Warden dashboard (shell available now)
cd dashboard
cp .env.example .env # fill in the Supabase URL and anon key
npm install
npm run dev # open http://localhost:5173The pages are placeholders until Phase 10. The dashboard shows which variables are missing if .env is incomplete.
Student app (from Phase 4)
Expo Go shows the screens that don't use native code. Anything that uses the Keystore, Wi-Fi or Play Integrity needs a development build.
cd app
npm install
npx expo start # Metro, scan the QR code with Expo Go for screens
npx eas-cli build --profile development --platform android # once: a dev build to install on the phone
npx expo start --dev-client # then open the dev build on the phoneThe phone and the laptop must be on the same Wi-Fi. Set EXPO_PUBLIC_API_BASE_URL to the laptop's LAN address, not 10.0.2.2 (that address only works in the Android emulator).
Press Ctrl+C in each terminal. Upstash Redis keeps running in the cloud, so there is nothing to stop for it.
NightRoll/
├── app/ Expo student app (Android, Kotlin Expo Modules)
├── backend/ FastAPI service: windows, nonces, gates, scoring, review
├── face/ Face worker: embeddings and anti-spoof (internal)
├── dashboard/ React warden dashboard
├── supabase/ Supabase CLI project and SQL migrations
├── infra/ Docker Compose and deployment files
├── config/ Room map example (real map stays local, git-ignored)
├── scripts/ One-off tools and Phase 0 risk spikes
├── docs/ PRD, TRD, architecture
└── phase/ Build plan, one file per phase
The design is settled (see docs/ and phase/). Phase 0, base setup, is in progress: folders, env examples, the Upstash Redis setup and the setup steps are in place. No feature code exists yet.
Released under the Apache License 2.0.
Team Zero_Latency
![]() Rahul L S @Rahul-8283 |
![]() Jivites @djivites |
![]() Sukanthan @sukanthan06 |
![]() Kesav Satya Sai Nimmagadda @kesavvvvvv |



