The Google Civic Information API representativeInfoByAddress was shut down April 2025.
Cicero (by Melissa) is the best replacement — it covers all levels of U.S. government:
federal, state (governor + both legislative chambers), county, and municipal.
The current codebase has the schema scaffolding (User.streetAddress, Official, UserDistrict)
but no automated lookup — UserDistrict records are manually seeded, and the Ask page
shows all officials rather than filtering to the user's representatives.
Create a new service module following the established pattern in action-network.ts:
getApiKey()/isEnabled()guard functions (graceful skip when key unset)ciceroFetch(params)— HTTP GET tohttps://app.cicerodata.com/v3.1/officialwithsearch_loc(full address string),key(API key), and optionaldistrict_typefilterlookupOfficialsByAddress(address)— accepts{ street, city, state, zip }, calls Cicero, returns a normalized array of officials with their district info- Type definitions for the Cicero API response shape
Cicero returns officials grouped by district. Each official includes:
first_name, last_name, party, office.title, office.chamber,
district.district_type (e.g. NATIONAL_UPPER, STATE_LOWER, LOCAL),
district.label, plus contact fields (email, url, phone, photo).
We map district.district_type to our chamber enum:
| Cicero district_type | Our chamber value |
|---|---|
NATIONAL_UPPER |
senate |
NATIONAL_LOWER |
house |
STATE_EXEC |
state_exec |
STATE_UPPER |
state_senate |
STATE_LOWER |
state_house |
LOCAL / LOCAL_EXEC / COUNTY |
local |
- Add
ciceroId(String, optional, unique) toOfficialmodel — the Ciceroskprimary key used for deduplication when upserting officials from API responses - Add
level(String, optional) toOfficial— stores the raw Cicerodistrict_typefor richer filtering (e.g. "county_legislature" vs "city_council") - Add
addressLookedUpAt(DateTime, optional) toUser— timestamp of last Cicero lookup, used to avoid burning credits on repeat lookups for the same address - Expand
chamberenum comment to include"state_exec"for governors/lt. governors - Run
npx prisma migrate devto generate migration
A client component with a simple form:
- Street address, city, state (dropdown of 50 states + DC), zip code
- "Find My Officials" submit button
- On submit: POST to new
/api/addressroute - Shows a loading spinner while Cicero lookup runs
- On success: displays the list of matched officials, then redirects to home
- On error: shows a user-friendly message
Server-side API route:
requireAuth()— must be signed in- Validate address fields (street, city, state, zip all required)
- Call
lookupOfficialsByAddress()from the new Cicero module - For each official returned:
prisma.official.upsert()keyed onciceroId— create if new, update contact info if existing
- Delete existing
UserDistrictrecords for this user (address changed) - Create new
UserDistrictrecords linking user to each matched official - Update
Userwith address fields + setisAddressVerified = true+ setaddressLookedUpAt = now() - Return the list of matched officials
In src/auth.ts, add a signIn callback:
- After successful OAuth sign-in, check if the user has
isAddressVerified === false - If so, set the redirect to
/addressinstead of/ - Returning users with a verified address go straight to home
Update src/app/api/officials/route.ts:
- If the user is authenticated and has
isAddressVerified, query only officials linked viaUserDistrictfor that user - If not authenticated or no address, fall back to returning all officials (or return empty with a prompt to complete address registration)
Update src/app/ask/page.tsx:
- Remove the "In production, this list will be limited..." placeholder text
- If no officials returned (user hasn't entered address), show a call-to-action
linking to
/address
- Add to
.env.examplewith documentation comment - Add to
.env(blank, for local dev graceful skip)
src/__tests__/cicero.test.ts— unit tests for response normalization and chamber mapping, using mocked fetch responsessrc/__tests__/address-api.test.ts— integration test for the/api/addressroute, verifying officials are upserted and UserDistrict records created
- Add
ciceroIdvalues to existing seeded officials - Add state-level and local officials to the seed to demonstrate multi-level coverage
- Update
UserDistrictmappings to reflect realistic multi-level representation (each seeded user gets ~8-12 officials: 2 US senators, 1 US rep, governor, 1 state senator, 1 state rep, county exec, city council member, etc.)
| File | Change |
|---|---|
src/lib/cicero.ts |
New — Cicero API client |
prisma/schema.prisma |
Add ciceroId, level to Official; addressLookedUpAt to User |
src/app/address/page.tsx |
New — address collection form |
src/app/api/address/route.ts |
New — address submission + Cicero lookup |
src/auth.ts |
Add redirect logic for unverified addresses |
src/app/api/officials/route.ts |
Filter to user's officials via UserDistrict |
src/app/ask/page.tsx |
Remove placeholder text, add address CTA |
.env.example |
Add CICERO_API_KEY |
.env |
Add CICERO_API_KEY (blank) |
prisma/seed.ts |
Add ciceroId + multi-level officials |
src/__tests__/cicero.test.ts |
New — Cicero client tests |
src/__tests__/address-api.test.ts |
New — address route tests |
Steps 1-2 first (foundation), then 3-4 (address flow), then 5-6 (integration into existing flows), then 7-9 (config/tests/seed).