How I Structured My Firestore Data Model for an Anti-Proxy Attendance System

How I Structured My Firestore Data Model for an Anti-Proxy Attendance System

3 9
calendar_today agoschedule7 min read

The database architecture behind students, meetings, attendance, authentication, and controlling unnecessary Firestore reads.*


How I Structured My Firestore Data Model for an Anti-Proxy Attendance System

When I began building my attendance system, database architecture wasn't really my first concern.

I had one straightforward objective:

Make the MVP work.

I needed a place to store students.

I needed a way to create meetings.

I needed to save attendance records.

So I started creating Firestore data structures based on whatever the application needed at that moment.

Initially, that approach worked.

Then the application started expanding.

Authentication was introduced.

Dynamic QR authentication was introduced.

Device verification was introduced.

Attendance history was introduced.

Offline synchronization was introduced.

And suddenly, Firestore was doing much more than simply holding attendance records.

It was turning into the foundation that the rest of the application depended on.

That's when an important realization hit me:

A schema that is perfectly fine for an MVP may not remain suitable once the application starts growing.


The First Question: What Really Needs to Live in the Database?

An attendance system sounds straightforward until you actually list everything it needs to keep track of.

For every attendance event, I may need details such as:

  • The student
  • The meeting
  • The attendance status
  • The time the attendance was recorded
  • Authentication information
  • Device information
  • Synchronization state
  • Administrative information

The difficult part wasn't simply finding a place to store all of these things.

The harder problem was determining how all of these pieces should connect with one another.

I started looking at the database through the entities that actually existed inside the system.

Student
   │
   ├── Identity
   ├── Device
   └── Attendance
          │
          ▼
        Meeting
          │
          ├── QR Session
          └── Attendance Records

The database needed to represent these relationships without making the application repeatedly retrieve the same information.


Firestore Forces a Different Way of Thinking

Firestore doesn't work like a traditional relational database.

There are no conventional SQL joins where I can simply write something like:

SELECT ...
FROM students
JOIN attendance ...
JOIN meetings ...

Instead, I need to think about a different question:

Which pieces of data does the application need to access together?

That question became one of the most important considerations while designing the schema.

In Firestore, the structure of your data directly influences how much information the application has to retrieve.

And I had already experienced the consequences of getting that wrong.


I Still Remember the 50,000 Read Problem

Earlier during development, I accidentally burned through my Firestore free-tier reads while testing and debugging the application.

That experience completely changed the way I looked at database operations.

Before that happened, my thinking was basically:

"It's just one additional read."

Afterwards, my question became:

"Does the application actually need this read?"

That shift in mindset started influencing the database architecture as the project continued to evolve.


Designing Around Actual Access Patterns

Rather than beginning with:

"Which collections should I create?"

I started approaching the problem from the opposite direction:

"What information does the application actually need to retrieve?"

For example, when an attendance session is running, the system needs enough information to validate the request and then record the attendance correctly.

So the database has to support operations such as:

Create Meeting
      ↓
Generate Attendance Session
      ↓
Validate Student
      ↓
Validate Device
      ↓
Validate QR
      ↓
Record Attendance

The database should not become the thing slowing down every stage of the process.


Keeping Attendance as a Separate Record

One of the key decisions I made was to treat attendance as its own record rather than repeatedly changing a student's profile whenever they attend a meeting.

Conceptually, the relationship looks like this:

Student
   │
   └──────────────┐
                  │
                  ▼
              Attendance
                  │
                  ▼
                Meeting

This allows attendance history to exist separately from the student's core profile.

Instead of only asking:

"Is this student present?"

the system can answer a much more useful question:

"Was this student present for this particular meeting, and when exactly was that attendance recorded?"

That distinction is important.

Attendance represents an event, rather than simply being a permanent property attached to a student.


More Data Does Not Automatically Mean a Better Database

A common trap during database design is duplicating information everywhere because it makes individual queries feel easier.

At first, that can seem convenient.

The problem appears later when those duplicated values need to stay consistent.

For example, copying the same information into several documents may simplify one specific query, but now the application has to maintain multiple versions of the same data.

So I constantly found myself balancing:

Fast reads

against

Data duplication

against

Write complexity

against

Firestore usage

There isn't one universal answer that solves all of these concerns.

The right decision depends on how the application actually reads and writes the data.


What an Attendance Request Actually Contains

At a high level, an attendance request now carries several different pieces of context.

Attendance Request
       │
       ├── Student Identity
       │
       ├── Meeting
       │
       ├── QR Validation
       │
       ├── Device Verification
       │
       └── Attendance Record

So attendance isn't really just:

student → present

It's closer to:

student
   +
meeting
   +
valid session
   +
trusted device
   +
validated request
   =
attendance

That means the database is not merely a storage layer.

It becomes part of the security architecture as well.


Thinking Carefully About Reads and Writes

After experiencing the 50,000-read issue, I started paying much more attention to the difference between reads and writes.

A system can function correctly and still perform far more database operations than it actually needs.

For example:

Page Load
   ↓
Read Student
   ↓
Read Meeting
   ↓
Read Configuration
   ↓
Read Student Again
   ↓
Read Meeting Again

Nothing is technically failing here.

But the application is doing unnecessary work.

So I started looking for ways to:

  • Reuse information that the application already has
  • Eliminate repeated reads
  • Cache frequently accessed information
  • Minimize unnecessary writes
  • Fetch only the data that is actually required

This eventually became part of the larger optimization work I covered in Part 3.


Firestore Security Goes Beyond Security Rules

Another lesson from the project was that database security cannot be reduced to Firestore Security Rules alone.

Rules are important.

But they represent only one layer of the overall security model.

In my attendance system, validation happens across several stages:

User Authentication
        ↓
Dynamic QR Validation
        ↓
Device Verification
        ↓
Backend Validation
        ↓
Firestore
        ↓
Attendance Record

The database should never blindly trust whatever information the frontend submits.

The backend needs to validate the request before writing sensitive attendance information.

This separation became even more important as more anti-proxy mechanisms were introduced into the system.


Offline Support Changes the Data Lifecycle

Then another challenge appeared.

Offline support.

Once the system could record attendance without immediately communicating with Firestore, the data model needed to distinguish between two different states:

Attendance created locally

and

Attendance successfully synchronized

Conceptually, the lifecycle became:

Student
   ↓
Attendance Event
   ↓
Local Storage
   ↓
Pending Sync
   ↓
Firestore
   ↓
Synchronized

That meant the lifecycle of an attendance record was no longer simply:

Create → Save

It became:

Create
  ↓
Store locally
  ↓
Wait
  ↓
Synchronize
  ↓
Confirm

That changed the way I thought about reliability as well.


What I Would Avoid Doing

One of the clearest lessons from this project is that Firestore schemas should not be designed by blindly copying relational database patterns.

Firestore has its own strengths, limitations, and way of handling data.

Instead of asking:

"How would I structure this in MySQL?"

I found it much more useful to ask:

"What are the operations my application performs most often?"

Then I could design the data model around those access patterns.

That single shift in perspective made the database design much easier to reason about.


The Bigger Lesson From the Data Model

At the beginning, the database felt like infrastructure.

Something beneath the actual application.

But as the system became more complex, I realized that the data model affects almost every major part of the application:

  • Performance
  • Cost
  • Security
  • Offline synchronization
  • Backend architecture
  • Query complexity
  • Scalability

A poor data model doesn't necessarily cause an obvious failure on day one.

Sometimes the problem is much quieter.

It simply makes every new feature a little harder to build.

And that can become a much bigger problem over time.


What I Would Change Today

If I were starting this system again today, I would spend considerably more time thinking about the data model before implementing features.

I'd define:

Entities
   ↓
Relationships
   ↓
Access Patterns
   ↓
Read / Write Frequency
   ↓
Caching Strategy
   ↓
Security Boundaries

before writing the first database query.

I learned this lesson after the system was already functioning.

Building first showed me what the application genuinely needed.

But if I rebuilt it from scratch, I'd bring those lessons into the design phase much earlier.


Final Thoughts

The original requirement sounded simple:

"I just need somewhere to store attendance."

Over time, that requirement evolved into something much larger:

"I need a data architecture that can support authentication, dynamic QR validation, device verification, offline synchronization, and efficient reads and writes."

Those are two completely different problems.

And probably one of the biggest lessons this project gave me is that database design isn't simply about deciding where data should be stored.

It's about defining how the entire application communicates with that data.

The database isn't simply sitting underneath the architecture.

It is part of the architecture.


What's Next?

The database was only one part of the overall system.

The next challenge was infrastructure.

My backend was running on a free-tier service, and that introduced another constraint that became difficult to ignore:

cold starts.

Which raised another question:

How do you make a slow backend feel fast from the user's perspective?

That's the problem I'll explore next.


🔗 Project

GitHub:

https://github.com/siddarthpatelkama/UBA-veltech-attendance-system


💬 Your turn

If you've built a Firestore application before, what's one database decision you made early that later saved you from dealing with a much bigger problem?

I'd love to hear about it.

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelski - Apr 23

Delivering Database Changes

Steve Fentonverified - Jul 22

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

The Hidden Program Behind Every SQL Statement

lovestaco - Apr 11
chevron_left
1.1k Points12 Badges
hyderabad,telangana,India.github.com/siddarthpatelkama
6Posts
3Comments
9Connections
Pre-final year B.Tech Computer Science student passionate about AI, full-stack development, and soft... Show more

Related Jobs

View all jobs →

Commenters (This Week)

1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!