HomeCase studiesA Cross-Platform Mobile App for Relationship-Based Civic...

Case study · Bristol, United Kingdom

A Cross-Platform Mobile App for Relationship-Based Civic Outreach

Civic engagement and grassroots campaign organising — the practice of asking supporters to contact people they already know rather than strangers

Outreach from a stranger is ignored; outreach from someone already in your contacts is not. Our team built a Flutter mobile application that turns that insight into a workable process: it reads a volunteer's own address book, helps them match people they know against public electoral roll records through a fast swipe-and-confirm flow, and then hands them a campaign-supplied message to send from their own phone. Campaign wording, button labels and message templates are all delivered by the server, so an organisation can change its entire outreach script without shipping an app release. Delivered from a single Dart codebase to both iOS and Android.

Industry
Civic engagement and grassroots campaign organising — the practice of asking supporters to contact people they already know rather than strangers
Solution
A cross-platform mobile application built in Flutter, operating as the client for a purpose-built REST API. Identity is delegated to a hosted provider and application telemetry to a hosted monitoring service.
Platforms
Android · iOS · Cross-platform · Web & API
Stack
Flutter · OAuth
Location
Bristol, United Kingdom · delivered remotely
Role
Engineering, with the team behind for mobile and QA

Delivered remotely for a business based in Bristol, United Kingdom. Names withheld by agreement.

Project Overview

Industry: Civic engagement and grassroots campaign organising — the practice of asking supporters to contact people they already know rather than strangers.

Type of solution: A cross-platform mobile application built in Flutter, operating as the client for a purpose-built REST API. Identity is delegated to a hosted provider and application telemetry to a hosted monitoring service.

Business context: Campaign outreach has historically meant cold contact from a purchased list — calls and doorsteps from someone the recipient has never met, with the conversion rate that implies. Relationship-based organising takes the opposite approach: a short message from a friend, a cousin or a former colleague carries weight that no call centre can manufacture. The obstacle is entirely operational. A volunteer's phone holds hundreds of contacts, and nothing in it indicates who is registered, who already participates reliably, and who would benefit from a reminder. Working that out by hand is not something any volunteer will do.

General users: Volunteers and supporters running outreach from their own devices. Campaign and organisation administrators configure outreach actions and message wording through the backend rather than in the app.

General purpose: To make relationship-based outreach practical on a phone — turning an unstructured address book into a short, prioritised list of people worth contacting, and putting a ready-to-send message behind each one.

The Business Challenge

An address book is not a target list. Hundreds of contacts, no structure, no indication of which ones matter. Without a way to narrow it, the volunteer is asked to do research before they can do outreach — and most will simply not start.

Matching informal contacts to formal records is genuinely hard. An address-book entry might be a first name and a mobile number. An electoral roll record is a full legal name, address, age and participation history. Joining the two is an identity-resolution problem, and getting it wrong is not a minor bug — messaging the wrong person about their voting record is a reputational incident for the campaign.

Common names return unusable results. Search on a frequent surname with nothing else to qualify it and the result set is enormous. A mobile app cannot render it and a volunteer cannot read it.

A list that never shortens gets abandoned. If every session presents the same hundreds of contacts, the work feels infinite. The list has to visibly shrink as the volunteer makes decisions.

Campaign wording changes constantly; app releases do not. Message scripts are revised weekly during an active campaign — sometimes daily. An app that hard-codes its copy is out of date before it clears review.

The message has to come from the volunteer, not from a platform. The entire premise is personal connection. A message arriving from an unfamiliar shortcode is a cold contact wearing a friend's name, and recipients can tell.

Personal data belongs to the volunteer. The app is asking for access to a person's private address book. Whatever the platform does with that access has to be minimal, obvious and defensible.

Two mobile platforms, one budget. Volunteers use whatever phone they own. Building and maintaining two native applications was not proportionate to the scope.

Our Approach

Choose Flutter for genuine single-codebase delivery. One Dart codebase produces both iOS and Android applications with a consistent interface, one set of business logic and one release process. For a product of this scope — device access, list interaction, network calls and native handoffs — Flutter delivers the whole surface without platform-specific code beyond configuration and permissions.

Keep the human in the matching loop. We deliberately did not auto-match contacts to records. The app narrows candidates aggressively and then asks the volunteer to confirm the right one, because the volunteer is the only party who actually knows which person this is. Automation here optimises for the wrong thing: it converts an easy human decision into a hard machine guess with a costly failure mode.

Make narrowing progressive and immediate. The candidate search pre-fills from what the address book already knows and re-runs the moment the volunteer adds anything further — a region, an approximate age, a birthday they happen to remember. Each addition cuts the result set. The volunteer never types a query; they answer what they know.

Treat an oversized result set as its own state, not an error. Rather than paginating through thousands of irrelevant candidates, the app recognises a too-broad search as a distinct condition and tells the volunteer to add a qualifier. This is a small design decision that removes the single most frustrating dead end in the flow.

Make the list consume itself. Swipe one way to queue a contact for matching, the other to set them aside. Both decisions persist on the device, and already-matched contacts are subtracted server-side. Every session leaves the list shorter than it was.

Move the campaign's voice to the server. Tab labels, screen titles, per-contact prompts and the message body itself are delivered by the API and keyed by action type, with sensible built-in defaults as a fallback. Re-wording an entire outreach programme is a backend configuration change, not a release. This is the decision that keeps the app useful across campaign cycles.

Hand off to the device's own messaging. Outreach opens the native SMS composer with the campaign's text already in place. The volunteer reviews it, edits it if they want to, and sends it from their own number. The platform never becomes a bulk-messaging system, and the message keeps the personal provenance that makes it work.

Minimise what leaves the phone. Address-book contents stay on the device. Only opaque contact identifiers and confirmed record associations are exchanged with the backend, and enrichment data is joined for display at the point of use.

Instrument from the first release. Application telemetry — navigation traces and structured error capture on every network and authentication path — was built in from the start, because the people using this app are volunteers who will abandon a broken screen rather than report it.

The Solution

Delegated authentication. Sign-in and sign-up run through a hosted identity provider's managed login flow, with separate paths for returning and new users, and provisioning against the backend on first registration. A cold-start routing gate decides between the login screen and the main application before the first frame is drawn.

Device contact access with explicit consent handling. Read-only address-book permission is requested, and a clear permission-denied state is rendered rather than a blank or broken screen.

Swipe-based contact triage. The full address book is presented as a searchable, refreshable list. A swipe in one direction queues a contact for matching; the other sets them aside. Both are visually signposted with coloured backgrounds and persisted locally.

Live search across contacts. Text filtering narrows the list as the volunteer types, for the common case where they already have someone in mind.

Candidate record search. A dedicated matching screen pre-populates from the address-book entry and lets the volunteer add whatever additional detail they know, re-running the search on every change and showing enough context on each candidate to disambiguate confidently.

Oversized-result handling. A too-broad search is surfaced as its own recognised state prompting refinement, rather than as an error or an unusable list.

One-tap association. Confirming a candidate links that address-book entry to the record and removes the contact from the pending queue.

Configured outreach actions. Two outreach types — a participation reminder and an invitation to join as a volunteer — each with their own screen. Every piece of copy on them, including the message body, is supplied by the backend.

Native message handoff. Outreach opens the device SMS composer with the campaign message pre-filled, addressed to the contact's own number.

Contact detail view. Per-contact activity history showing what outreach has already happened and when, alongside a participation history grid spanning seven election cycles — so the volunteer can see whether this is someone who always participates or someone who genuinely needs the reminder.

Affiliation indicator. Each matched contact carries a visual affiliation marker, giving the volunteer immediate context before they open anything.

Application telemetry. Navigation traces and structured error reporting throughout, providing field visibility into how the application behaves on real devices.

Key Features

Address-book triage by swipe The volunteer's own contacts, presented as a working queue with a two-way swipe decision per row, searchable and refreshable, with decisions persisted so the list shortens with use.

Human-confirmed record matching The app narrows candidates and the volunteer confirms — deliberately avoiding automated matching in a domain where a false positive means contacting the wrong person about personal information.

Progressive search refinement Candidate search pre-fills from the contact and re-runs on every additional detail the volunteer supplies, converting an open-ended query into a series of small confirmations.

Oversized-result guard Too-broad searches are recognised as a distinct state prompting refinement, rather than returning an unusable result set to a mobile device.

Server-driven outreach configuration Button labels, screen titles, per-contact prompts and message templates are all delivered by the API and keyed by action type, so campaign wording changes without an app release.

Native SMS handoff Messages open in the device's own composer, pre-filled and editable, sent from the volunteer's own number — preserving the personal provenance that makes relationship-based outreach effective.

Participation history at a glance A compact grid across seven election cycles showing prior participation, so the volunteer can tell a reliable participant from someone worth a nudge before writing a word.

Per-contact activity timeline A dated record of outreach already carried out against each contact, preventing duplicate contact and giving the volunteer continuity between sessions.

Delegated identity Hosted OAuth2/OIDC login with a managed authentication flow and platform credential integration, rather than a hand-built authentication stack in the app.

Built-in field telemetry Navigation traces and structured error capture from the first release, giving visibility into an application used by non-technical volunteers who will not file bug reports.

Technical Architecture

Presentation layer. A Flutter application using the Material widget set, organised as one screen widget per functional area — contact triage, matching queue, candidate search, two outreach action screens, contact detail and profile. Navigation combines a five-destination bottom navigation shell with named-route push navigation for detail and matching screens, and a navigation restoration scope so state survives the operating system reclaiming the app in the background.

Routing gate. A dedicated entry widget resolves the persisted session state on cold start and redirects to either the authentication screen or the main shell before the first meaningful frame, avoiding the flash of an incorrect screen.

Service layer. Five focused collaborators: an API client owning all backend communication and JSON mapping; an authentication wrapper around the hosted identity SDK; a local file-persistence helper for triage decisions and session state; a telemetry service; and a shared loading-and-error overlay helper used across screens.

Model layer. Plain Dart model classes with hand-written JSON factory constructors covering the user and their configured actions, candidate records, matched contacts with enrichment data, and per-contact action history — including a composite model that pairs a server record with its corresponding device contact for display.

Device integration layer. Read-only address-book access with explicit permission handling, and URL-scheme launching for native SMS handoff, with the required schemes declared in the iOS application configuration.

Backend integration. A JSON REST API hosted on a managed cloud application platform, consumed over HTTPS. All identity resolution, roll data, campaign configuration and message templating are server responsibilities; the client renders what the server defines.

Configuration-driven presentation. The user payload carries the campaign's configured actions — code, button text, prompt text and message template — which the client resolves at render time with built-in defaults as fallback, so a missing or new configuration degrades gracefully rather than breaking a screen.

Flow: Device address book → local swipe triage with persisted decisions → candidate search against the REST API → volunteer-confirmed association → matched contacts enriched with participation and action history → native SMS composer on the volunteer's own device → telemetry to hosted monitoring

Technology Stack

Category Technology
Mobile framework Flutter
Language Dart
Delivered platforms iOS and Android from a single codebase
UI toolkit Material Design widget set, light and dark themes declared
State management Built-in Flutter widget state, with a shared application-level session and configuration store
Navigation Named routes with generated route handling, bottom navigation shell, navigation state restoration
Networking Dart HTTP client with hand-written JSON model mapping
Authentication Hosted OAuth2 / OIDC identity provider SDK with managed login and sign-up flows
Platform credential integration iOS associated domains for web credential handoff; native keychain support via the identity SDK
Device capabilities Read-only contacts access; URL-scheme launching for native SMS, telephony and mail
Local persistence Application documents storage via the platform path provider
Monitoring Hosted application telemetry service with buffered event transmission
Internationalisation Flutter localisation with ARB-based message generation
Code quality Flutter lint rule set with static analysis configuration
Backend (integrated, not in scope) Bespoke JSON REST API on a managed cloud application platform

Technical Challenges & Solutions

Challenge Our Approach
Joining informal address-book entries to formal register records, where a wrong match is a reputational incident rather than a bug Deliberately kept the human in the loop: the app narrows candidates aggressively using what the contact record already contains plus anything the volunteer can confirm, then requires an explicit confirmation tap. Automation was rejected here because the volunteer holds knowledge no matching algorithm has.
Common names producing result sets too large to render or read Treated an oversized result as a first-class application state with its own guidance to refine, rather than paginating through thousands of irrelevant candidates or surfacing a generic error.
Volunteers abandoning a contact list that never gets shorter Two-way swipe triage with both decisions persisted on the device, combined with server-side subtraction of already-matched contacts, so the working list visibly consumes itself across sessions.
Campaign message wording changing far faster than app store release cycles Moved all outreach copy — button labels, screen titles, per-contact prompts and message bodies — to server-supplied configuration keyed by action type, resolved at render time with built-in defaults as fallback. Re-wording a campaign is a backend change.
Preserving the personal provenance that makes relationship-based outreach work Handed off to the device's native SMS composer with the message pre-filled and editable, so it sends from the volunteer's own number. The platform deliberately does not become a messaging gateway.
Requesting access to a person's private address book responsibly Read-only permission with an explicit denied state, contact details never transmitted, and only opaque identifiers and confirmed associations exchanged with the backend.
Showing a volunteer whether a contact actually needs a reminder A compact participation-history grid spanning seven cycles alongside a dated timeline of outreach already carried out, so prioritisation and duplicate-contact avoidance both happen at a glance.
Two mobile platforms on a scope that could not justify two native builds A single Flutter codebase producing both applications, with platform-specific work confined to signing, permissions, entitlements and URL scheme declarations.
Diagnosing problems in an app used by volunteers who will not report bugs Telemetry built in from the first release, with navigation traces and structured error capture on every network and authentication failure path.

Security & Reliability

Delegated identity. Authentication runs through a hosted identity provider's managed login flow rather than credential handling built into the application, with distinct sign-in and sign-up paths and platform-level credential integration on iOS through an associated-domains entitlement.

Minimal data egress. Address-book contents remain on the device. Only opaque contact identifiers and confirmed record associations are exchanged with the backend, and enrichment data is joined for display at the point of use rather than accumulated on the client.

Explicit permission handling. Contacts access is requested read-only, and refusal is rendered as a clear application state rather than a failure — the app never silently misbehaves because a permission was declined.

Server-side authority. Record matching, roll data and campaign configuration are all backend responsibilities. The client holds no dataset and no business rules that could be tampered with locally.

Graceful configuration fallback. Every server-supplied label and message resolves against a built-in default, so an incomplete or newly added configuration degrades to sensible copy instead of an empty or broken screen.

Error visibility. Structured error capture on every network and authentication path, with contextual properties attached, so failures occurring on volunteers' own devices are visible to the delivery team rather than invisible.

Navigation state restoration. A restoration scope allows the navigation stack to be rebuilt after the operating system reclaims the application in the background — relevant on a device that switches to the SMS composer and back on every outreach action.

Scalability & Performance

Refinement instead of pagination. The matching flow narrows queries at source rather than transferring and rendering large result sets, keeping payloads small and the interface responsive regardless of how common a name is.

Lazy list construction. Contact and match lists build their rows on demand, so memory and layout cost track what is on screen rather than the size of the underlying list.

Local decision state. Triage decisions are persisted on the device and applied client-side, removing a round trip from the most frequent interaction in the product.

Buffered telemetry. Events are batched and transmitted with a bounded timeout, so monitoring never blocks the interface or consumes bandwidth in tight loops.

A deliberately thin client. No local database, no offline sync engine and no client-side dataset — the scale burden sits in the backend where it can be scaled independently, and the mobile application stays small, fast to start and cheap to maintain.

Stateless sessions. Durable state lives server-side against the authenticated user, so a volunteer replacing or reinstalling on a device resumes without a migration path.

Native handoff instead of in-app messaging. Delegating message delivery to the device removes an entire class of throughput, deliverability and queueing concerns from the product.

Business Outcomes

  • Relationship-based outreach became practical on a phone, turning an unstructured personal address book into a short, prioritised working list.
  • Matching accuracy is protected by design, because the person who actually knows the contact makes the final call rather than an algorithm guessing.
  • Volunteers see progress, with a list that shortens as they work rather than resetting to the same hundreds of contacts every session.
  • Campaign messaging can be revised at campaign speed, with wording, labels and templates changed centrally without an app release or store review.
  • Outreach retains its personal character, arriving from the volunteer's own number through the device's own messaging rather than from a platform.
  • Duplicate and misdirected contact is reduced, because prior outreach and participation history are visible before the volunteer acts.
  • Both mobile platforms are served from one codebase, keeping delivery and ongoing maintenance proportionate to the product's scope.
  • The delivery team has field visibility, through telemetry built in from the first release rather than added after problems surfaced.

Why it worked

The hardest decisions in this project were not technical, and the technical ones followed from getting them right. The central question was whether to automate the match between a phone contact and a formal record. Automating it would have demonstrated more engineering. It would also have been wrong — because the failure mode is contacting the wrong person about information they consider private, and because the volunteer already holds the knowledge that resolves the ambiguity. We built the software to assist that judgement rather than replace it.

The same thinking shaped the rest. Campaign copy lives on the server because campaign copy changes weekly and app releases do not. Messages send through the device's own composer because the personal provenance is the product, and a platform-sent message is just a cold contact with better data behind it. Contact details never leave the phone because asking for someone's address book carries an obligation, and the smallest defensible answer is the right one.

Our team works across Flutter and Dart for genuine cross-platform mobile delivery, hosted identity integration, REST API consumption, native device capability work — contacts, permissions, URL-scheme handoffs, platform entitlements — and configuration-driven interfaces that let a client change their product without changing their app. Just as importantly, we bring the judgement to tell which parts of a workflow must stay human, which should be moved out of the release cycle, and which data simply should not be collected.

Final Summary

A campaign's most effective outreach is a short message from someone the recipient already knows. The obstacle has never been the idea; it is that a volunteer's phone holds hundreds of contacts with no indication of which of them matter, and no volunteer is going to work that out by hand.

Our team built a Flutter application that closes that gap. It reads the volunteer's own address book, presents it as a swipe-triaged working queue that shortens with use, and helps them match the people they know against public electoral roll records through progressive refinement and an explicit confirmation — deliberately keeping the human in the loop, because a wrong match here means contacting the wrong person about personal information. Once matched, each contact carries their participation history and a record of outreach already carried out, so the volunteer can tell at a glance who needs a reminder and who does not.

Around that sit the decisions that keep the product usable over time. Every piece of campaign copy — button labels, prompts and the message text itself — is delivered by the server and keyed by action type, so an organisation can re-word an entire outreach programme without shipping a release. Messages open in the device's own composer and send from the volunteer's own number, keeping the personal provenance that makes the approach work and keeping the platform out of the bulk-messaging business. Address-book contents never leave the phone. And it is one Dart codebase serving both iOS and Android, with telemetry built in from the first release so problems on volunteers' devices are visible to the people who can fix them.

01 — Questions

asked about this kind of project

Why choose Flutter for a cross-platform mobile app?

Because for a product whose surface is lists, forms, network calls and native handoffs, Flutter delivers the whole thing from one Dart codebase with a consistent interface on both platforms. You write the business logic once, ship one release process, and confine platform-specific work to signing, permissions, entitlements and URL scheme declarations. The alternative — two native codebases — doubles the maintenance for a product whose differences between platforms are configuration, not behaviour.

When is Flutter the wrong choice?

When the product's value is in platform-specific capability that the plugin ecosystem does not cover well — heavy real-time media processing, deep OS integration, or performance-critical graphics where you would end up writing native code on both sides anyway. Flutter's advantage is a shared codebase; if most of your code has to be platform-specific regardless, that advantage disappears and you should build native.

How do you handle device contacts in a mobile app responsibly?

Request read-only access, render a clear state when it is declined rather than failing silently, keep contact details on the device, and transmit the minimum the backend genuinely needs — typically opaque identifiers rather than names and numbers. Also write a permission prompt that says what you actually do with the data: a vague one both fails app store review and, more importantly, deserves to.

Should a mobile app match records automatically or ask the user to confirm?

It depends entirely on the cost of a false positive. Where a wrong match is trivially reversible, automate it. Where a wrong match means acting on the wrong person's personal information, keep the human in the loop and use the software to narrow the candidates instead. In this project the user held knowledge no matching logic could access, so the correct design was assistance, not automation.

How do you change app copy without shipping a new release?

Deliver it from the server as configuration and resolve it at render time, with built-in defaults as a fallback so a missing or new configuration degrades to sensible text rather than an empty screen. This matters enormously wherever content changes faster than the app store review cycle — campaigns, promotions, seasonal messaging, regulated wording. It is a small amount of work at build time that removes a recurring release dependency for the life of the product.

Should an app send messages itself or hand off to the device?

Hand off, unless messaging is the product. Using the device's native composer means the message sends from the user's own number, arrives with genuine personal provenance, and can be edited before it goes. It also keeps an entire category of problems — deliverability, throughput, queueing, sender reputation and messaging compliance — out of your product entirely.

Should you build authentication yourself or use a hosted provider?

Use a hosted provider in almost every case. Managed login flows give you sign-in, sign-up, recovery, session handling and platform credential integration as a configured capability rather than code you own and must keep secure. Building it yourself is justified only when your identity requirements are genuinely unusual, and far fewer products meet that bar than believe they do.

How much telemetry should a first release include?

More than feels necessary. Navigation traces and structured error capture on every network and authentication path cost very little to add up front and are the difference between diagnosing a field problem and guessing at it — particularly for an app used by non-technical people who will abandon a broken screen rather than report it. Retrofitting telemetry after a bad launch is always more expensive than having it from day one.

03 — Similar project?

describe what is different about yours

Build yours.

Tell me what exists, what you need and the deadline. Fixed price for a defined scope, or hourly from $10 with a written estimate first.

Ahmedabad, India · IST (UTC+5:30) · --:-- IST · Mon–Fri 09:00–18:00 IST · US & EU overlap daily

No newsletter, no CRM. Just a reply.