All ForACity subdomains share the same codebase and database. Routing is handled client-side.
Business development — entity, compliance, contracts, market strategy
Route: /bizdevDeveloper portal — architecture, API docs, setup guide (this page)
Route: /developerMain civic feed — social feed, officers, legislation, meetings
Route: /These are all remaining items that need implementation. They are grouped by what's blocking them. Each includes the files to edit, code patterns, and step-by-step instructions.
What: Browser push notifications via Web Push API + optional email digest. Users get alerted about upcoming meetings, new conflicts, and legislation matching their Values Compass profile.
Effort: ~4-6 hours
Step 1: Generate VAPID Keys
# Install web-push globally
npm install -g web-push
# Generate VAPID key pair
web-push generate-vapid-keys
# Add to Manus Settings → Secrets:
# VAPID_PUBLIC_KEY=BPxr...
# VAPID_PRIVATE_KEY=nD2x...
# VAPID_SUBJECT=mailto:[email protected]Step 2: Add server-side push service
// server/pushNotification.ts
import webpush from 'web-push';
import { env } from './_core/env';
webpush.setVapidDetails(
env.VAPID_SUBJECT,
env.VAPID_PUBLIC_KEY,
env.VAPID_PRIVATE_KEY
);
export async function sendPush(subscription: PushSubscription, payload: {
title: string; body: string; url?: string;
}) {
return webpush.sendNotification(subscription, JSON.stringify(payload));
}Step 3: Add subscription table to schema
// drizzle/schema.ts — add:
export const pushSubscriptions = mysqlTable('push_subscriptions', {
id: int('id').primaryKey().autoincrement(),
userId: int('user_id').notNull(),
endpoint: text('endpoint').notNull(),
p256dh: text('p256dh').notNull(),
auth: text('auth').notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
// Then run: pnpm db:pushStep 4: Add tRPC routes
// server/routers.ts — add to notificationRouter:
subscribe: protectedProcedure
.input(z.object({
endpoint: z.string(),
keys: z.object({ p256dh: z.string(), auth: z.string() })
}))
.mutation(async ({ ctx, input }) => {
// Save subscription to pushSubscriptions table
// Return success
}),
unsubscribe: protectedProcedure.mutation(async ({ ctx }) => {
// Delete subscription for ctx.user.id
}),Step 5: Frontend service worker
// client/public/sw.js
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/favicon.ico',
data: { url: data.url }
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.notification.data.url) {
event.waitUntil(clients.openWindow(event.notification.data.url));
}
});What: The Facebook connect flow is already fully coded in the backend and frontend. It just needs a real Facebook App ID to activate. This enables profile photo sync, friend discovery, and social sharing.
Effort: ~1-2 hours (mostly Facebook Developer Console setup)
Step 1: Register Facebook App
# Go to: https://developers.facebook.com/apps/
# Click "Create App" → "Consumer" type
# App name: ForACity
# Add product: "Facebook Login"
# Settings → Basic:
# App ID: (copy this)
# App Secret: (copy this)Step 2: Configure OAuth Redirect
# In Facebook App → Facebook Login → Settings:
# Valid OAuth Redirect URIs:
https://foracity.com/api/facebook/callback
https://www.foracity.com/api/facebook/callback
https://levineact-cozcqk6f.manus.space/api/facebook/callbackStep 3: Set Environment Variables
# In Manus Settings → Secrets, add:
FACEBOOK_APP_ID=your_app_id_here
FACEBOOK_APP_SECRET=your_app_secret_hereStep 4: Test the Flow
# 1. Log in to ForACity
# 2. Go to your profile or settings
# 3. Click "Connect Facebook"
# 4. Authorize the app
# 5. Verify: profile photo syncs, friends appear
# 6. Test sharing a feed post to FacebookWhat: Scheduled daily job that pulls new campaign filings from NetFile, auto-flags contributions exceeding the $500 Levine Act threshold, generates admin alerts, and creates feed posts for significant new contributions.
Effort: ~6-8 hours
Sub-task A: Scheduled Job (Daily NetFile Pull)
// server/jobs/netfileSync.ts
import { fetchNetFileTransactions } from '../netfile';
import { getOfficers, createContribution } from '../db';
import { notifyOwner } from '../_core/notification';
export async function runNetFileSync() {
const officers = await getOfficers({ jurisdictionId: 1 }); // Escondido
for (const officer of officers) {
if (!officer.netfileFilerId) continue;
const txns = await fetchNetFileTransactions(officer.netfileFilerId);
// Filter new transactions since last sync
// Insert into contributions table
// Check for $500+ threshold
}
await notifyOwner({ title: 'NetFile Sync Complete', content: '...' });
}
// Trigger via cron or admin buttonSub-task B: Auto-Flag $500+ Contributions
// In the sync loop, after inserting contribution:
if (contribution.amount >= 250) {
// Run conflict detection for this officer
await detectConflicts(officer.id);
// Create admin alert
await createNotification({
userId: adminUserId,
type: 'conflict_alert',
title: `High contribution: $${amount} to ${officer.name}`,
body: `From ${contributorName}. Levine Act threshold exceeded.`,
});
}Sub-task C: Auto-Create Feed Posts
// After processing significant contributions:
await createFeedPost({
postType: 'contribution',
title: `New contribution: $${amount} to ${officer.name}`,
body: `${contributorName} contributed $${amount}...`,
jurisdictionId: officer.jurisdictionId,
officerId: officer.id,
});Sub-task D: Admin Dashboard Indicator
// The NetFile sync status indicator already exists in
// AdminDashboard.tsx and AdminNetFile.tsx
// Just need to update the lastSync timestamp after each run:
await updateNetFileSyncStatus(officerId, {
lastSync: new Date(),
recordCount: newRecords,
status: 'success',
});What: Replace the current database-based representative lookup with real GIS polygon queries for state assembly, state senate, congressional, and county supervisor districts. Add a map overlay showing district boundaries.
Effort: ~8-12 hours
Sub-task A: Query SD County GIS for State/Federal Districts
// ArcGIS FeatureServer endpoints for San Diego County:
// City Council Districts (already working):
// https://services2.arcgis.com/eJcVbjTyyZIzZ5Ye/arcgis/rest/services/CouncilDistricts/FeatureServer/0
// State Assembly Districts:
// https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/USA_118th_Congressional_Districts/FeatureServer/0
// State Senate Districts:
// https://services2.arcgis.com/[org]/arcgis/rest/services/CA_State_Senate_Districts/FeatureServer/0
// Congressional Districts:
// https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/USA_118th_Congressional_Districts/FeatureServer/0
// Query pattern (point-in-polygon):
const url = `${featureServerUrl}/query?` + new URLSearchParams({
geometry: `${lng},${lat}`,
geometryType: 'esriGeometryPoint',
spatialRel: 'esriSpatialRelIntersects',
outFields: '*',
f: 'json',
});Sub-task B: Replace LLM Inference with GIS Queries
// server/routers.ts — update address.lookup:
// Currently uses database lookup for sphere of influence
// Replace with real GIS queries:
async function queryDistrictFromGIS(lat: number, lng: number, featureServerUrl: string) {
const res = await fetch(`${featureServerUrl}/query?` + new URLSearchParams({
geometry: `${lng},${lat}`,
geometryType: 'esriGeometryPoint',
spatialRel: 'esriSpatialRelIntersects',
outFields: '*',
f: 'json',
}));
const data = await res.json();
return data.features?.[0]?.attributes;
}
// Call for each level:
const [assembly, senate, congress, supervisor] = await Promise.all([
queryDistrictFromGIS(lat, lng, ASSEMBLY_URL),
queryDistrictFromGIS(lat, lng, SENATE_URL),
queryDistrictFromGIS(lat, lng, CONGRESS_URL),
queryDistrictFromGIS(lat, lng, SUPERVISOR_URL),
]);Sub-task C: Cache District Boundary Data
// Option 1: In-memory cache (simple)
const districtCache = new Map<string, { data: any; expires: number }>();
function getCachedDistrict(key: string) {
const cached = districtCache.get(key);
if (cached && cached.expires > Date.now()) return cached.data;
return null;
}
// Option 2: Database cache table
export const districtCache = mysqlTable('district_cache', {
id: int('id').primaryKey().autoincrement(),
lat: decimal('lat', { precision: 10, scale: 7 }),
lng: decimal('lng', { precision: 10, scale: 7 }),
districtType: varchar('district_type', { length: 50 }),
districtData: json('district_data'),
expiresAt: timestamp('expires_at'),
});Sub-task D: Map Overlay with District Boundaries
// client/src/pages/AddressLookup.tsx
// Use the Google Maps component from client/src/components/Map.tsx
import { MapView } from '@/components/Map';
// In the results section, add a map:
<MapView
onMapReady={(map, google) => {
// Add district boundary polygons
const districtLayer = new google.maps.Data();
districtLayer.loadGeoJson(districtGeoJsonUrl);
districtLayer.setStyle({
fillColor: '#CF8A2E',
fillOpacity: 0.15,
strokeColor: '#CF8A2E',
strokeWeight: 2,
});
districtLayer.setMap(map);
// Add marker for searched address
new google.maps.Marker({
position: { lat, lng },
map,
title: 'Your Address',
});
}}
/>What: Email/push reminders 1 day and 1 hour before events the user RSVP'd to. Auto-generate event summary after the event ends and link to related legislation and officer profiles.
Effort: ~4-6 hours
Sub-task A: Email Reminder Service
// Option 1: Use Manus notifyOwner for admin alerts (already works)
// Option 2: Add SendGrid/Resend for user emails:
// server/emailService.ts
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendReminder(to: string, meeting: Meeting) {
await resend.emails.send({
from: 'ForACity <[email protected]>',
to,
subject: `Reminder: ${meeting.title} tomorrow`,
html: `<h2>${meeting.title}</h2>
<p>${meeting.location} at ${formatTime(meeting.date)}</p>
<a href="https://foracity.com/meetings/${meeting.id}">View Details</a>`,
});
}
// Env var needed: RESEND_API_KEYSub-task B: Scheduled Reminder Check
// server/jobs/meetingReminders.ts
export async function checkAndSendReminders() {
const now = Date.now();
const oneDayFromNow = now + 24 * 60 * 60 * 1000;
const oneHourFromNow = now + 60 * 60 * 1000;
// Find meetings happening in ~24h or ~1h
const meetings = await getUpcomingMeetings();
for (const meeting of meetings) {
const timeUntil = meeting.date - now;
if (timeUntil < oneDayFromNow && timeUntil > oneDayFromNow - 300000) {
// Send 24h reminder to all RSVPs
const rsvps = await getRsvpsForMeeting(meeting.id);
for (const rsvp of rsvps) {
await sendReminder(rsvp.user.email, meeting);
}
}
}
}Sub-task C: Auto-Generate Post-Event Summary
// After a meeting's date has passed, auto-generate summary:
// This already works via the AI meeting summary feature.
// Just need to trigger it automatically:
// In the scheduled job:
const pastMeetings = await getPastMeetingsWithoutSummary();
for (const meeting of pastMeetings) {
if (!meeting.aiSummary && meeting.agendaItems) {
// Trigger the existing generateSummary route
await generateMeetingSummary(meeting.id);
// This already auto-posts to feed (implemented)
}
}What: Route escondido.foracity.com to the admin portal. The admin dashboard already exists at /admin — this just needs subdomain routing and the domain added in Manus.
Effort: ~30 minutes
Step 1: Add Domain in Manus
# In Manus Management UI:
# Settings → Domains → Add Domain
# Enter: escondido.foracity.com
# Follow DNS instructions to point to ManusStep 2: Add Subdomain Routing
// client/src/App.tsx — already has subdomain detection
// Just add escondido.foracity.com to the routing logic:
const hostname = window.location.hostname;
if (hostname === 'escondido.foracity.com') {
// Auto-redirect to /admin
return <Redirect to="/admin" />;
}County Board of Supervisors — Separate Area with County Branding
Create a dedicated section for SD County supervisors with county-specific colors and layout. Currently they appear in the jurisdiction switcher alongside cities. Low priority since the data is already accessible.
Files: client/src/pages/ (new CountyPortal.tsx), client/src/App.tsx (new route)
Mobile Viewport Testing (375px / 390px)
Systematic testing on iPhone SE (375px) and iPhone 14 (390px) viewports. The UI is already mobile-first with 44px touch targets and responsive grids. This is a QA pass, not a code change.
How: Chrome DevTools → Device Toolbar → iPhone SE / iPhone 14. Test all pages.
Scrape Voting Records from City Council Minutes
Parse PDF meeting minutes from the city website to extract individual council member votes on each agenda item. Currently voting records are seeded with sample data. This requires PDF parsing (pdf-parse npm package) and pattern matching.
Source: https://www.escondido.org/city-council-agendas-and-minutes → Download PDF minutes → Parse vote tables
| # | Item | Blocker | Effort | Priority |
|---|---|---|---|---|
| 1 | Web Push notification service | VAPID keys | 4-6h | High |
| 2 | Register Facebook App | FB Developer Console | 1h | Medium |
| 3 | Configure FB OAuth redirect | FB App created | 15m | Medium |
| 4 | Activate FB connect with real App ID | FB App ID env var | 15m | Medium |
| 5 | Test FB flow end-to-end | FB App live | 1h | Medium |
| 6 | Scheduled NetFile daily sync job | Cron service | 3h | High |
| 7 | Auto-flag $500+ contributions | Sync job running | 1h | High |
| 8 | Generate admin alerts for high-risk | Sync job running | 1h | High |
| 9 | Auto-create feed posts for contributions | Sync job running | 1h | Medium |
| 10 | NetFile sync status indicator | None (UI exists) | 30m | Low |
| 11 | Query GIS for state/federal districts | Find ArcGIS URLs | 3h | Medium |
| 12 | Replace DB lookup with GIS queries | GIS endpoints | 2h | Medium |
| 13 | Cache district boundary data | GIS working | 2h | Low |
| 14 | Map overlay with district boundaries | GIS working | 3h | Medium |
| 15 | RSVP email/push reminders | Email service (Resend/SendGrid) | 3h | Medium |
| 16 | Auto-generate post-event summary | Cron service | 2h | Medium |
| 17 | escondido.foracity.com subdomain routing | Domain added in Manus | 15m | High |
| 18 | Add escondido.foracity.com domain | DNS setup | 15m | High |
| 19 | County BoS separate branding area | None (deferred) | 4h | Low |
| 20 | Mobile viewport testing (375/390px) | None (QA pass) | 2h | Low |
| 21 | Scrape voting records from PDF minutes | PDF parsing | 6h | Medium |
The marketing site and kanban board were built in Telegram but cannot be deployed from there. Each app needs its own Manus web project to bind custom domains and publish. The code is already in the GitHub monorepo — it just needs to be set up as standalone Manus projects.
Create a new Manus web project
Go to manus.im → New Task → Ask: "Create a new web project for the ForaCity marketing site"
Pull code from GitHub monorepo
# In the new Manus project sandbox:
git clone https://github.com/Renderlyapp/foracity.git temp
cp -r temp/marketing/* .
rm -rf tempInstall dependencies and verify it builds
pnpm install
pnpm buildBind custom domain
Management UI → Settings → Domains → Add "marketing.foracity.com"
Save checkpoint and publish
Save a checkpoint → Click Publish in the Management UI header
CNAME (already set in GoDaddy): marketing → foracrm-6t6jggqx.manus.space
GitHub path: /marketing/ in Renderlyapp/foracity
Current Manus URL: https://foracrm-6t6jggqx.manus.space
Create a new Manus web project
Go to manus.im → New Task → Ask: "Create a new web project for the ForaCity kanban board"
Pull code from GitHub monorepo
# In the new Manus project sandbox:
git clone https://github.com/Renderlyapp/foracity.git temp
cp -r temp/projects/* .
rm -rf tempInstall dependencies, push DB schema, and verify
pnpm install
pnpm db:push
pnpm buildBind custom domain
Management UI → Settings → Domains → Add "projects.foracity.com"
Save checkpoint and publish
Save a checkpoint → Click Publish in the Management UI header
CNAME (already set in GoDaddy): projects → foracitykanban-8p8cpdcz.manus.space
GitHub path: /projects/ in Renderlyapp/foracity
Current Manus URL: https://foracitykanban-8p8cpdcz.manus.space
This project (levine-act-guide) is already deployed and bound to legal.foracity.com. No action needed.
CNAME: legal → foracomply-3v8zasyd.manus.space
Status: Live and published
These CNAME records should already be set in GoDaddy DNS for foracity.com:
| Type | Name | Value |
|---|---|---|
| CNAME | marketing | foracrm-6t6jggqx.manus.space |
| CNAME | legal | foracomply-3v8zasyd.manus.space |
| CNAME | projects | foracitykanban-8p8cpdcz.manus.space |
Important: Make sure GoDaddy proxy is OFF (DNS only) for SSL to work with Manus hosting.
Manus projects built via Telegram don't surface in the web dashboard's Management UI, which is the only place where custom domains can be bound and projects can be published. Each subdomain needs its own Manus web project with its own hosting instance. The workaround is to create new web projects, pull the code from the GitHub monorepo, and deploy from the web UI. This has been reported as a bug to the Manus team.