Vibe-Coding Civic Infrastructure: How I Built a Municipal Event Manager in One Weekend

Written by nabilahaahim14 | Published 2026/02/26
Tech Story Tags: artificial-intelligence | ai-governance | govtech-solutions | case-study | firebase | digitaltransformation | vibe-coding | firebase-genkit-angular-app

TLDRGovernment technology often suffers from 'spreadsheet chaos'-high-stakes tasks like budget tracking and vendor compliance managed through manual, error-prone files. Drawing on my training from the One Million Prompters initiative, I built Eventide: an AI-enhanced civic infrastructure tool. Using Firebase Studio and Google Genkit, I engineered a 'vibe-coded' solution that automates: Financial Transparency: Real-time auditing of estimated vs. actual municipal spend. Liability Management: Automatic tracking of vendor insurance (COIs) and revenue splits. AI Conflict Detection: An LLM-powered engine that prevents logistical gridlock between community events. This is a blueprint for how modern AI tools can solve 'contextually complex' government problems in a fraction of the time of traditional GovTech vendors.via the TL;DR App

I work in local government, which means I spend a lot of time thinking about things like "Certificate of Insurance expiration tracking" and "vendor conflict detection across multiple community events." Riveting stuff, I know.


But here's what most people don't realize about municipal event planning: it's basically full-stack project management under regulatory constraints with public money. When our village runs 15+ community events a year-from farmers markets to holiday parades-the spreadsheet chaos gets real.


So I did what any AI-trained government analyst would do: I vibe-coded a solution in Firebase Studio.

The Problem: Event Management in Government Isn't Like Private Sector

When a tech company throws an event and something goes wrong, they eat the cost and move on. When a village throws an event and something goes wrong, it's a line item in next year's audit and possibly a question at a council meeting.

The stakes are different. The requirements are different:


  1. Budget transparency: Every dollar needs to be tracked from "potential" to "paid"
  2. Vendor compliance: COIs expire. If a vendor's insurance lapses before your event, you're liable
  3. Conflict detection: You can't book the same vendor for two events on the same day (ask me how I know)
  4. Revenue share tracking: Some events involve sponsorships with predetermined splits between the village and event coordinators
  5. Audit trails: Everything needs to be exportable, timestamped, and defensible


Existing event management tools are built for conferences or weddings. They're not built for the weird intersection of public finance, regulatory compliance, and community programming.

The Solution: Eventide (Event + Tide... get it?)

I built Eventide in Firebase Studio using Google's Genkit framework. The goal was simple: create something that could replace our Frankenstein's monster of shared Excel files, email chains, and post-it notes.


// Server Actions for data mutations
// Using Next.js App Router + TypeScript

export async function createEvent(formData: FormData) {
  const eventData = {
    title: formData.get('title') as string,
    description: formData.get('description') as string,
    date: formData.get('date') as string,
    location: formData.get('location') as string,
    totalBudget: parseFloat(formData.get('budget') as string),
    status: 'Active' as EventStatus
  };
  
  const newEvent = await addEvent(eventData);
  revalidatePath('/');
  return newEvent;
}

The beauty of Next.js Server Actions is that I don't need separate API endpoints. The client calls server functions directly, mutations happen server-side, and the UI auto-refreshes. For a solo government developer working nights and weekends, this is gold.

The Budget State Machine

This is where it gets interesting. In government, budget items don't just exist-they transition through states with specific rules:

type BudgetItemStatus = 'Potential' | 'Waiting' | 'Confirmed' | 'Paid';

interface BudgetItem {
  id: string;
  eventId: string;
  category: string;
  vendor: string;
  estimatedCost: number;
  actualCost?: number;
  status: BudgetItemStatus;
  poNumber?: string;      // Only exists when Confirmed or Paid
  invoiceLink?: string;    // Added when converting to PO
  notes?: string;
}

function calculateBalance(budget: BudgetItem[], totalBudget: number) {
  const estimated = budget.reduce((sum, item) => 
    sum + item.estimatedCost, 0);
  
  const actual = budget.reduce((sum, item) => 
    sum + (item.status === 'Paid' ? item.actualCost! : 0), 0);
  
  return {
    estimatedBalance: totalBudget - estimated,
    actualBalance: totalBudget - actual
  };
}

Why this matters: When I'm mid-event and the council asks "Where are we on budget?", I can give them two numbers:


  • Estimated balance: What we'll have left if all pending costs come through
  • Actual balance: What we've actually spent


This distinction is critical. Estimated tells me if we can afford another vendor. Actual tells me what's hit the village's bank account.


AI-Powered Conflict Detection

This is where Genkit earns its keep. I needed to detect scheduling conflicts between events, but the logic isn't simple:


  • Two events on the same day = obvious conflict
  • Same vendor booked for overlapping dates = conflict
  • Two outdoor events within 3 hours of each other at different locations = potential conflict (setup/breakdown/travel time)


Rather than hard coding these rules, I used a Genkit flow:

import { defineFlow, runFlow } from '@genkit-ai/flow';
import { gemini15Pro } from '@genkit-ai/googleai';
import { z } from 'zod';

const conflictAnalysisFlow = defineFlow(
  {
    name: 'vendorConflictAnalysis',
    inputSchema: z.object({
      event1: z.object({
        title: z.string(),
        date: z.string(),
        location: z.string(),
        vendors: z.array(z.string())
      }),
      event2: z.object({
        title: z.string(),
        date: z.string(),
        location: z.string(),
        vendors: z.array(z.string())
      }),
      includeArchived: z.boolean()
    }),
    outputSchema: z.object({
      hasConflict: z.boolean(),
      conflictType: z.enum([
        'date_overlap',
        'vendor_double_booking',
        'travel_time_conflict',
        'none'
      ]),
      details: z.string(),
      recommendation: z.string()
    })
  },
  async (input) => {
    const prompt = `
      Analyze these two municipal events for potential conflicts:
      
      Event 1: ${input.event1.title}
      Date: ${input.event1.date}
      Location: ${input.event1.location}
      Vendors: ${input.event1.vendors.join(', ')}
      
      Event 2: ${input.event2.title}
      Date: ${input.event2.date}
      Location: ${input.event2.location}
      Vendors: ${input.event2.vendors.join(', ')}
      
      Consider:
      1. Date/time overlap
      2. Vendor double-booking
      3. Travel time between locations (assume 30min setup, 30min breakdown)
      
      Respond with conflict assessment and recommendations.
    `;
    
    const result = await gemini15Pro.generate({ prompt });
    
    return {
      hasConflict: result.hasConflict,
      conflictType: result.conflictType,
      details: result.details,
      recommendation: result.recommendation
    };
  }
);

export async function analyzeConflicts(event1Id: string, event2Id: string) {
  const event1 = await getEvent(event1Id);
  const event2 = await getEvent(event2Id);
  
  return await runFlow(conflictAnalysisFlow, {
    event1: {
      title: event1.title,
      date: event1.date,
      location: event1.location,
      vendors: event1.vendors.map(v => v.name)
    },
    event2: {
      title: event2.title,
      date: event2.date,
      location: event2.location,
      vendors: event2.vendors.map(v => v.name)
    },
    includeArchived: false
  });
}


Real-world example: We had a summer concert series and a farmers market that both used the same sound equipment vendor. The AI caught that the concert was scheduled 2 hours after the market ended, but flagged it as a travel-time conflict because the venues were 25 minutes apart. We adjusted the concert start time by 30 minutes. Crisis averted.


The Revenue Share Automation

This feature is oddly specific but incredibly valuable. Some of our events (like sponsored 5K runs) have revenue-sharing arrangements. When a sponsor pays, the money gets split 50/50 between the village and the event coordinator.

interface Sponsorship {
  id: string;
  eventId: string;
  sponsorName: string;
  amount: number;
  deliverables: {
    rcvdLogo: boolean;
    formToNabila: boolean;
    formLink?: string;
    invoiceSent: boolean;
    invoiceLink?: string;
    rcvdPayment: boolean;
  };
  revenueShare?: {
    villageShare: number;
    coordinatorShare: number;
  };
}

function updateSponsorshipPayment(sponsorship: Sponsorship, paid: boolean) {
  if (paid) {
    sponsorship.revenueShare = {
      villageShare: sponsorship.amount * 0.5,
      coordinatorShare: sponsorship.amount * 0.5
    };
  } else {
    delete sponsorship.revenueShare;
  }
  return sponsorship;
}


When someone checks the "Rcvd. Payment" box, two new columns appear showing the split. Finance gets their number, the coordinator gets theirs, and I don't have to manually calculate it in a spreadsheet at 11 PM the night before a council meeting.


Why This Matters for GovTech

I built this in about 72 hours of vibe-coding across a long weekend. Not because I'm particularly fast, but because modern tools (Next.js App Router, Genkit, ShadCN UI) let you build production-grade applications without bikeshedding over architecture for weeks.

The lesson for civic tech:

Most government software problems aren't technically complex. They're contextually complex. The hard part isn't the code-it's understanding the regulatory requirements, the audit trails, the compliance needs, and the weird edge cases that only emerge when you're managing public money.


Traditional GovTech vendors charge $50K+ for solutions like this because they're selling to procurement departments with formal RFP processes. But a lot of mid-sized municipalities can't justify that cost for event management.


So we end up with spreadsheets. Which means we end up with errors. Which means we end up with audit findings.

Eventide proves you can build government-grade tools with consumer-grade development speed.


The Code: https://github.com/nabilahaahim14/EventTide

// src/app/events/[id]/page.tsx

import { getEvent } from '@/lib/actions';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import BudgetTab from '@/components/BudgetTab';
import TasksTab from '@/components/TasksTab';
import VendorsTab from '@/components/VendorsTab';
import SponsorshipsTab from '@/components/SponsorshipsTab';
import GraphicsTab from '@/components/GraphicsTab';

export default async function EventPage({ 
  params 
}: { 
  params: { id: string } 
}) {
  const event = await getEvent(params.id);
  
  const { estimatedBalance, actualBalance } = calculateBalance(
    event.budgetItems,
    event.totalBudget
  );
  
  return (
    <div className="container mx-auto p-6">
      <div className="mb-8">
        <h1 className="text-3xl font-bold">{event.title}</h1>
        <p className="text-muted-foreground">{event.location} • {event.date}</p>
      </div>
      
      <div className="grid grid-cols-3 gap-4 mb-8">
        <BalanceCard 
          title="Total Budget" 
          amount={event.totalBudget} 
        />
        <BalanceCard 
          title="Estimated Balance" 
          amount={estimatedBalance}
          variant={estimatedBalance < 0 ? 'danger' : 'default'}
        />
        <BalanceCard 
          title="Actual Balance" 
          amount={actualBalance}
          variant={actualBalance < 0 ? 'danger' : 'success'}
        />
      </div>
      
      <Tabs defaultValue="budget">
        <TabsList>
          <TabsTrigger value="budget">Budget</TabsTrigger>
          <TabsTrigger value="tasks">Tasks</TabsTrigger>
          <TabsTrigger value="vendors">Vendors</TabsTrigger>
          <TabsTrigger value="sponsorships">Sponsorships</TabsTrigger>
          <TabsTrigger value="graphics">Graphics</TabsTrigger>
        </TabsList>
        
        <TabsContent value="budget">
          <BudgetTab eventId={event.id} items={event.budgetItems} />
        </TabsContent>
        
        <TabsContent value="tasks">
          <TasksTab eventId={event.id} tasks={event.tasks} />
        </TabsContent>
        
        <TabsContent value="vendors">
          <VendorsTab eventId={event.id} vendors={event.vendors} />
        </TabsContent>
        
        <TabsContent value="sponsorships">
          <SponsorshipsTab eventId={event.id} sponsorships={event.sponsorships} />
        </TabsContent>
        
        <TabsContent value="graphics">
          <GraphicsTab eventId={event.id} assets={event.graphicsAssets} />
        </TabsContent>
      </Tabs>
    </div>
  );
}

What's Next

Current version uses in-memory storage for rapid prototyping. Next steps:

  • Firebase Firestore integration for real persistence
  • Multi-tenant architecture (other villages have asked to use this)
  • Email notifications when vendor COIs are about to expire
  • Calendar integration (iCal export for event schedules)
  • Mobile-responsive task management (currently desktop-optimized)


The code is functional. The architecture is sound. The value is proven-we're using it for real events right now.



Written by nabilahaahim14 | A software engineer and AI strategist with a focus on high-performance web systems and AI integration
Published by HackerNoon on 2026/02/26