agents pwa firebase the-hub

Building a Chat App for My AI Agents

Why I replaced Discord with a custom Angular PWA for talking to my AI agents, and the Firebase IAM gotcha that almost derailed the deploy.

February 14, 2026 · 7 min read · Michael Schilling
A translucent glass chat bubble with three glowing orbs inside representing AI agents in conversation

Part 4 of the series: Building The Hub

Six days after deploying Nightcrawler, I replaced Discord as the primary interface. Not because Discord was bad — it worked fine. But once you start building a system with its own data model and its own agents, you want the interface to be part of that system too.

The problem with Discord

Discord served its purpose as a prototype interface. But it came with friction:

  • The data lived in Discord’s servers, not mine. Conversation history was locked in their platform.
  • Adding custom features (thinking indicators, capture integration, agent-specific UI) meant fighting Discord’s bot API limitations.
  • It felt disconnected from The Hub ecosystem. My knowledge base was in Obsidian, my agents ran on a homelab, but the conversation layer was on someone else’s platform.

I wanted the chat to be a first-class part of The Hub — same Firebase project, same data model, same deployment pipeline.

Two glass screens side by side — the left fragmenting with data flowing outward, the right solid with data flowing into a secure base

The companion apps idea

The day after deploying Nightcrawler, I realized something: an agent on a homelab container is powerful, but it needs interfaces. Not just Discord — proper mobile-first apps that extend The Hub into my daily workflow. A chat interface, a capture tool, maybe more later.

So before writing a single line of UI code, I spent an evening planning what I called “Hub Companion Apps” — a monorepo that would house all the mobile-first PWAs that plug into The Hub ecosystem. I used the architecture from another project I maintain as a blueprint: Nx workspace, Angular apps with shared libraries, Firebase for the backend. The key decision was to build it as a monorepo from day one, so apps could share a UI library, Firebase config, and deployment pipeline without duplicating anything.

I created a full implementation plan and handed it off to a team of three Claude Code agents running in parallel — one for scaffolding, one for infrastructure, one for the first app. The monorepo was standing within hours.

Hub Chat

Hub Chat was the first app built in this monorepo: an Angular PWA backed by Firebase Firestore. The tech stack was deliberately familiar — Angular is what I use professionally, Firebase handles real-time sync out of the box, and Tailwind CSS keeps the styling fast.

Shared libraries handle the theme, common components, and Firebase configuration across all companion apps. The PWA layer gives me offline capability and home screen installation on mobile.

The core feature is deceptively simple: real-time chat with AI agents. You send a message, it lands in Firestore, the agent on its homelab container picks it up via a listener, processes it through the LLM pipeline, and writes the response back. Firestore’s real-time listeners mean the response appears instantly in the UI — no polling, no WebSocket management.

Valentine’s Day deploy

February 14th was the big push. I deployed Hub Chat to production, and immediately hit a cascade of bugs that needed fixing in real-time.

The route param bug. I was using ngOnInit to read route parameters and load conversations. The problem? Angular’s effect() (from signals) was a better fit for reactive parameter changes, but mixing the two caused the conversation to not update when navigating between chats. Replacing ngOnInit with a proper effect() that tracked the route param fixed it cleanly.

Firestore security rules. Message deletion was failing silently. The Firestore rules allowed creating and reading messages but didn’t have a rule for deletes. A quick update to the security rules, deploy, fixed.

I spent time doing end-to-end UX testing using devtools-mcp — a browser automation tool that lets me script interactions and take screenshots. This surfaced 13 issues that I logged as a GitHub issue across high, medium, and low priority.

Three PRs, one day

The rest of the day was a sprint. Three PRs merged:

PR #16 added markdown rendering for agent responses (they write in markdown, so the chat needed to render it properly), a delete confirmation dialog, a thinking indicator that shows when the agent is processing, auto-scroll to the latest message, and relative timestamps (“2 min ago” instead of “14:32:07”).

PR #17 was pure visual polish. Chat bubble styling with proper alignment (user messages right, agent messages left), entrance animations so messages don’t just pop in, and a subtle glow ring on the input field when focused. Small details, but they make the difference between “functional prototype” and “app I actually want to use.”

PR #10 (in the hub-apps repo) added a camera freeze-frame capture feature with IndexedDB persistence. Not directly related to chat, but it shipped the same day as part of the broader Hub Apps push.

The Firebase IAM lesson

Here’s the gotcha that cost me an hour: deploying Hub Chat via GitHub Actions.

I set up a service account for Firebase deployment, gave it firebase.developAdmin permissions, configured the GitHub Action with google-github-actions/auth@v3, and… the deploy failed with a 403.

The confusing part? Authentication succeeded. The service account was valid, the token was issued, everything looked correct in the logs. But the actual Firebase deploy command returned a 403 Forbidden.

The missing piece: roles/serviceusage.serviceUsageConsumer.

This role isn’t included in any default Firebase role. It’s a Google Cloud Platform role that grants permission to use the project’s enabled services. Without it, you can authenticate against the project but you can’t actually call any of its APIs. The error message gives you no indication that this is the issue — it just says 403.

The fix is one line in your IAM setup:

gcloud projects add-iam-policy-binding PROJECT_ID \
  --member="serviceAccount:SA_EMAIL" \
  --role="roles/serviceusage.serviceUsageConsumer"

I’ve since hit this exact issue on every new Firebase project where I set up GitHub Actions deployment. It’s always the same missing role. If you’re setting up Firebase CI/CD and getting mysterious 403s after successful auth, check for this role first.

An unlocked glass padlock with a red barrier wall behind it — auth succeeded but access denied, with the missing key floating nearby

Custom domain

Hub Chat launched on a custom subdomain, later moved as part of consolidating my domains. The convention is function-based subdomains: one for Hub Chat, one for Hub Capture, one for the MCP Gateway. Clean, memorable, and each pointing to a different Firebase Hosting site or service.

Why build your own?

The question I get most is: “Why not just use ChatGPT / Claude.ai / whatever?” Three reasons:

  1. Data ownership. Every conversation lives in my Firestore. I can query it, export it, feed it back into other systems. Try doing that with ChatGPT conversation history.

  2. Multi-agent architecture. Hub Chat isn’t a wrapper around one LLM. It’s the interface to a system of agents, each with their own identity and capabilities. The data model supports both DMs (one-on-one with a specific agent) and channels (multiple agents in the same conversation).

  3. Integration surface. Because the chat is part of the same Firebase project as everything else, adding features is trivial. Thinking indicators, capture integration, agent delegation — these are Firestore writes, not API integrations.

Building your own chat app sounds like reinventing the wheel. And if all you need is a conversation with an LLM, it is. But when the chat is one layer in a larger system, owning that layer gives you leverage that no third-party interface can match.

Hub Chat became the primary way I interact with my agents. Discord still works as a fallback, but the PWA on my phone’s home screen is where the real conversations happen.

#agents #pwa #firebase #the-hub