Notes on building projects, technical decisions, and things I learned along the way.
August 2025
Firebase & Google Cloud Console — What I Actually Used
### Overview
Firebase is Google's mobile and web application platform built on top of Google Cloud infrastructure. While the Firebase console gives you a friendly UI for common tasks, the real power — and the real complexity — lives in the Google Cloud Console behind it. This post covers what I actually used, what surprised me, and what I'd do differently.
Firebase is not a separate product from Google Cloud — it is a curated layer on top of GCP. Every Firebase project is a Google Cloud project. You can manage the same resources from both consoles.
### Firebase Authentication
Firebase Authentication was the first thing I integrated. It handles email/password sign-up, Google Sign-In, and anonymous auth out of the box. In Flutter, the firebase_auth package wraps the SDK cleanly — FirebaseAuth.instance.signInWithEmailAndPassword() returns a UserCredential with the user's UID, which becomes the key for all Firestore documents.
Key insight: always use the Firebase UID as the primary key for user documents in Firestore. Never store a separate user ID — the UID is stable, unique, and already authenticated.
One gotcha: email verification is not enforced by default. I added a check on the User.emailVerified flag after sign-in and redirected unverified users to a verification screen. Firebase sends the verification email via user.sendEmailVerification() — no SMTP setup needed.
### Cloud Firestore
Cloud Firestore is a NoSQL document database with real-time sync. I used it to store device states, scene configurations, and user preferences in the IoT app. The data model is a collection of documents — each document is a map of key-value pairs, and documents can contain subcollections.
The real-time listener is the killer feature. FirebaseFirestore.instance.collection('devices').doc(deviceId).snapshots() returns a Stream that emits a new snapshot every time the document changes — anywhere in the world. In Flutter, wrapping this in a StreamBuilder gives you a UI that updates instantly when a device state changes.
Firestore security rules are the most important thing to get right. The default rules allow anyone to read and write — never ship with those. Write rules that check request.auth.uid == resource.data.userId for every user-owned document.
Firestore pricing is per read/write/delete operation, not per GB. A single snapshots() listener counts as one read per update. For a device that updates every second, that is 86,400 reads per day per listener — plan accordingly.
### Cloud Functions
Cloud Functions for Firebase are serverless Node.js functions that run in response to Firebase events or HTTP requests. I used them for two things: sending push notifications when a device state changed, and cleaning up orphaned documents when a user deleted their account.
The onDocumentUpdated trigger fires whenever a Firestore document changes. The function receives the before and after snapshots, so you can diff the state and decide whether to send a notification. Notifications go through Firebase Cloud Messaging (FCM) using the Admin SDK.
Cloud Functions cold starts are real. The first invocation after a period of inactivity can take 2–5 seconds. For latency-sensitive operations, keep functions warm with a scheduled ping or use minimum instance count (costs money).
### Google Cloud Console — What's Behind Firebase
The Google Cloud Console exposes everything Firebase hides. Things I needed it for:
- IAM & Service Accounts — creating service account keys for the Admin SDK in Cloud Functions and for local development
- Cloud Storage — Firebase Storage is backed by Google Cloud Storage; the GCC gives you bucket-level controls, lifecycle rules, and CORS configuration that the Firebase console doesn't expose
- Logs Explorer — Cloud Functions logs appear here with full structured logging; far more powerful than the Firebase console's log viewer
- Quotas & Billing — setting budget alerts so a runaway listener doesn't generate a surprise bill
- APIs & Services — enabling APIs like Cloud Vision, Cloud Translation, or Vertex AI that you want to call from Cloud Functions
Set a billing budget alert immediately after creating a Firebase project. Go to GCC → Billing → Budgets & Alerts → Create Budget. Set a threshold at $5 and $20. Firebase's free tier is generous but easy to exceed with real-time listeners at scale.
### Flutter Integration
The FlutterFire suite of packages makes Firebase integration straightforward. Initialise with Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform) in main() — the options are auto-generated by the flutterfire configure CLI command, which downloads the google-services.json and GoogleService-Info.plist and generates a Dart config file.
Never commit google-services.json or GoogleService-Info.plist to a public repository. They contain your Firebase project's API keys. Add them to .gitignore and use environment-specific files for different build flavours.
July 2025
Building an AI Student Assistant with MERN + RAG
### Overview
The AI Student Assistant lets students upload course documents and ask questions against them in real time. Instead of searching through PDFs manually, you ask a question and the system retrieves the most relevant passages and generates a contextual answer using a RAG pipeline.
The core architecture: documents are chunked → embedded → stored in a vector DB. At query time, the question is embedded, top-k chunks are retrieved by similarity, and those chunks are injected into the LLM prompt as context.
### The RAG Pipeline
When a user uploads a document, the backend splits it into chunks using LangChain's text splitters, generates embeddings via Bedrock's Titan Embeddings, and indexes them into OpenSearch k-NN. Each chunk is stored with metadata — document ID, user ID, page number, and chunk index.
Chunking strategy matters enormously. I settled on 512-token chunks with 64-token overlap after testing. Too-small chunks lose context; too-large chunks exceed the LLM's context window and dilute relevance scores.
At query time, the user's question is embedded using the same model, and a k-NN search retrieves the top-5 most semantically similar chunks. These are injected into a prompt template and sent to Claude via Bedrock's InvokeModelWithResponseStream API. The response streams back through Socket.IO token-by-token.
### Authentication with AWS Cognito
User authentication uses AWS Cognito with email + OTP verification. JWTs from Cognito are validated on the Express backend using aws-jwt-verify, which checks the token signature against Cognito's JWKS endpoint.
Cognito's hosted UI is convenient but inflexible. Building a custom auth UI with the Amplify SDK gives full UX control while keeping Cognito's security guarantees — token rotation, MFA, and brute-force protection are all handled server-side.
### Real-time Chat with Socket.IO
Each chat session is a Socket.IO room keyed by session ID. The server streams Bedrock's response token-by-token using InvokeModelWithResponseStream, emitting each token as a token event. Chat history is persisted in MongoDB with a TTL index — sessions expire after 30 days.
MongoDB TTL indexes are the cleanest way to auto-expire documents. Set expireAfterSeconds: 2592000 on a createdAt field and MongoDB's background thread deletes expired documents automatically — no cron job needed.
June 2025
IoT Home Automation with Flutter — BLE & Wi-Fi
### Overview
A Flutter mobile application for managing smart home channels, devices, and scenes. The app communicates with IoT hardware over BLE for initial pairing and Wi-Fi for remote control and state sync. Built as part of my internship at Apsis Solutions.
### Onboarding Flow
The onboarding flow has two paths: QR code and Wi-Fi. In the QR path, the user scans a QR code on the device using mobile_scanner, which encodes the device's BLE MAC address and a pairing token. The app connects over BLE, sends the Wi-Fi credentials, and the device joins the home network.
Android 12+ requires BLUETOOTH_SCAN, BLUETOOTH_CONNECT, and ACCESS_FINE_LOCATION for BLE operations. Request all three before any BLE call — missing even one causes a silent failure on some devices.
The permission_handler package made this manageable. I check and request permissions before any BLE operation and show a rationale dialog if the user denies.
### Device & Scene Management
Devices are Dart model classes with a type enum (switch, dimmer, sensor) and a state map. Scenes are stored locally as JSON and synced to Firestore on save. Activating a scene sends each device its target state in parallel using Future.wait.
Future.wait runs all futures concurrently and waits for all to complete. For activating a scene with 10 devices, this is ~10x faster than awaiting each device sequentially.
### BLE Reliability
BLE on Android is unreliable. Connection drops, GATT errors, and MTU negotiation failures are common. I added an exponential backoff retry loop for GATT operations and a connection watchdog that reconnects automatically if the device goes silent for more than 5 seconds.
Never assume a BLE connection is stable. Always implement a reconnection strategy. The pattern: on any GATT error, wait 2^attempt * 100ms, then retry up to 5 times before surfacing an error to the user.
May 2025
DDoS Detection in SDN using Machine Learning
### Overview
A machine learning system for detecting DDoS attacks in Software Defined Networks. SDN separates the control plane from the data plane, making it possible to collect fine-grained flow statistics from the controller — exactly the data ML models need.
### Dataset & Feature Selection
The dataset contains 100K+ network flow records labelled as normal or attack traffic. Each record has 23 features extracted from OpenFlow flow statistics: packet count, byte count, flow duration, inter-arrival time, packet rate, byte rate, and derived ratios.
Feature selection using
Scikit-learn's feature importances reduced 23 features to 12 while retaining 94% of predictive power. Fewer features = faster inference = lower latency in the real-time pipeline.
### Model Training
Compared Logistic Regression, Decision Tree, Random Forest, and Gradient Boosting. Random Forest gave the best results: 95.8% accuracy, 94.2% precision, 96.1% recall on the held-out test set. Trained with 100 estimators, max depth 20, and class-weight balancing.
The model is serialised with
joblib and loaded at Flask startup. Inference takes ~2ms per flow record on a standard CPU — fast enough for real-time classification at the SDN controller.
### Research Publication
Published in EPRA International Journal of Research and Development (IJRD), Vol. 10, Issue 8, August 2025. DOI: 10.36713/epra23698 ↗
April 2025
What I Learned Building Remindly & a Weather App in Flutter
### Remindly — Reminder App
Remindly stores reminders in SQLite via sqflite and schedules them as local notifications using flutter_local_notifications. State management uses the Provider pattern — a ReminderProvider wraps the SQLite repository and notifies listeners on any CRUD operation.
Android 12+ requires SCHEDULE_EXACT_ALARM for exact alarms. Always add a fallback to inexact alarms — they fire within a 15-minute window, which is acceptable for most reminder use cases and avoids a hard permission denial blocking the feature entirely.
### Weather App
Fetches current conditions and a 7-day forecast from the OpenWeatherMap API. Uses geolocator for GPS coordinates and json_serializable for parsing API responses into typed Dart models.
Always cache the last-fetched API response locally. Show stale data immediately on launch while fresh data loads in the background. A loading spinner on every launch feels slow even on fast connections — stale-while-revalidate is the right pattern here.
### Flutter Patterns I Settled On
After several Flutter projects, these are my defaults: Provider for state management, repository pattern for data access, named routes in main.dart, and const constructors everywhere possible.
const constructors are not just style — Flutter uses them to skip rebuilding widgets whose inputs haven't changed. A widget tree full of const constructors rebuilds significantly less on state changes.
March 2026
What I Learned at Apsis Solutions — My First Real Internship
### The Gap Between Side Projects and Real Software
Side projects are forgiving. You set the requirements, you decide when it's done, and nobody is waiting on your pull request. Professional software is different. At Apsis Solutions I learned what it actually means to write code that other people depend on — code that has to work on devices you've never touched, in conditions you didn't anticipate, for users who won't file a bug report, they'll just stop using the app.
The biggest shift: in a side project you write code until it works. In a professional setting you write code until it works, handles every edge case, is readable by your teammates, and doesn't break anything that was already working.
### Flutter in Production
I had built Flutter apps before the internship, but building them inside a product team is a different experience. The codebase had conventions — naming patterns, folder structure, widget decomposition rules — and understanding why those conventions existed took time. A widget that seems fine in isolation can cause performance problems when it's nested inside a list that rebuilds frequently.
I learned to think about widget trees differently. Every setState call triggers a rebuild of the widget and all its descendants. Keeping state as low in the tree as possible, using const constructors, and splitting large widgets into smaller ones are not style preferences — they are performance decisions.
Use Flutter DevTools' widget rebuild tracker before optimising. It shows exactly which widgets are rebuilding on each frame. I found a list item widget rebuilding 60 times per second because a parent was listening to a stream it didn't need to.
### REST API Integration Patterns
Integrating REST APIs in a side project means calling http.get() and parsing the JSON. In production it means handling every failure mode: network timeouts, 4xx errors, 5xx errors, malformed responses, and the case where the API returns 200 but the data is empty or null. I wrote a generic API client wrapper that handles all of these consistently and surfaces typed errors to the UI layer.
Never let raw API errors reach the UI. Map them to domain errors at the repository layer. The UI should only know about states: loading, success, and error — not about HTTP status codes or JSON parse exceptions.
### Git in a Team
I had used Git before but mostly as a backup tool — commit, push, done. Working in a team meant branching strategies, pull requests, code review, and merge conflicts. The discipline of writing clear commit messages, keeping PRs small and focused, and reviewing others' code carefully are skills that don't come from solo projects.
The most valuable thing I learned about code review: it is not about finding bugs. It is about sharing knowledge. When a reviewer asks why you did something a certain way, the answer either teaches them something or teaches you something. Either outcome is good.
### What I'd Tell Myself Before Starting
Read the existing code before writing new code. Understand the patterns in use before introducing new ones. Ask questions early — a 5-minute conversation saves hours of going in the wrong direction. And write tests, even simple ones, because they are the only way to know that a change you made three weeks later didn't break something you wrote today.
February 2026
Agentic AI in My Dev Workflow — What Actually Works
### How I Actually Use These Tools
I use four AI tools regularly and each has a distinct role. Amazon Q Developer lives in my IDE and handles inline completions, multi-file edits, and explaining unfamiliar code. Claude is my thinking partner for architecture decisions, code review, and writing documentation. OpenAI Codex and GitHub Copilot handle boilerplate generation and test scaffolding.
The key insight: these tools are not interchangeable. Each has a different strength. Using the right tool for the right task is itself a skill that takes time to develop.
### Amazon Q Developer — IDE-Native Intelligence
Amazon Q Developer is the tool I use most because it's where I spend most of my time — inside the editor. The inline completions are context-aware in a way that generic autocomplete isn't. It reads the surrounding code, understands the patterns in use, and suggests completions that fit the existing style rather than generic boilerplate.
The multi-file edit capability changed how I approach refactoring. Instead of manually updating every file affected by a change, I describe what I want and Q makes the edits across files simultaneously. A rename that would take 20 minutes of careful find-and-replace takes 2 minutes with a clear prompt.
Amazon Q's /dev command is particularly powerful for scaffolding. Describe the feature you want to build and it generates the file structure, boilerplate, and initial implementation. The output is never production-ready, but it's a solid starting point that saves the blank-page problem.
### Claude — Architecture and Thinking
I use Claude for decisions that require reasoning, not just pattern matching. When I'm designing a data model, choosing between two architectural approaches, or trying to understand why something is failing in a non-obvious way, Claude is where I go. The responses are longer and more reasoned than what you get from an IDE assistant.
Code review with Claude is genuinely useful. Paste a function and ask what could go wrong — it finds edge cases, security issues, and performance problems that I missed. It also explains why something is a problem, not just that it is one, which is how you actually learn.
The best prompt pattern I found for architecture decisions: describe the problem, list the constraints, and ask Claude to argue for and against each option before recommending one. The adversarial framing surfaces tradeoffs that a direct recommendation would skip.
### What I Had to Learn to Use These Tools Well
The biggest mistake I made early on was treating AI output as correct by default. It isn't. AI tools hallucinate APIs that don't exist, suggest patterns that are subtly wrong, and confidently produce code that compiles but doesn't do what you asked. The skill is not prompting — it's verification. You have to read every line of generated code as carefully as you'd read code from a junior developer.
Prompt engineering matters more than most people admit. Vague prompts produce vague output. Specific prompts — with context, constraints, and examples — produce output that's actually useful. I now spend as much time writing the prompt as I would have spent writing the code, and the result is usually better than what I'd have written alone.
The most useful prompt addition: "explain your reasoning step by step before giving the final answer." Chain-of-thought prompting produces more accurate output and makes it easier to spot where the reasoning went wrong.
### The Real Productivity Gain
The productivity gain from agentic AI is not that it writes code faster than you. It's that it eliminates the parts of development that don't require thinking. Boilerplate, repetitive patterns, documentation, test scaffolding, and routine refactoring — these are things that consume time without producing insight. When AI handles them, the time freed up goes to the parts that actually matter: understanding the problem, designing the solution, and making the decisions that require judgment.
A project that would have taken me three weeks to scaffold now takes three days. Not because the AI wrote three weeks of code in three days, but because I spent those three weeks on the 20% of the work that required real thinking, and the AI handled the other 80%.
Agentic AI is a force multiplier, not a replacement. It amplifies what you already know. If you don't understand the code it generates, you can't debug it, extend it, or take responsibility for it. The floor for using these tools effectively is still knowing how to code.