Feature spec
AdvancedCategory
communication-social
Build with AI
8–14 hours with Lovable or v0
Custom build
3–6 weeks custom dev
Running cost
$0–$50/mo up to 1K users
Works on
Everything it takes to ship an One-Click Contact Importer — parts, prompts, and real costs.
A contact importer lets users find friends already on your platform from their Google Contacts or phone address book. The build has four non-obvious requirements: OAuth scope `contacts.readonly` via a Supabase Edge Function, SHA-256 hashing of every phone/email before storage (never plaintext), match against pre-hashed user profiles, and GDPR-compliant 24-hour auto-deletion. With Lovable or v0 the implementation takes 8–14 hours and $0–$50/month depending on invite SMS volume.
What a One-Click Contact Importer Actually Is
A one-click contact importer lets users trigger a single button to import their Google Contacts (web) or device address book (mobile), instantly see which of their contacts are already on the platform, and invite the rest via email or SMS. The feature has two distinctly different implementations depending on platform: web apps use the Google People API with OAuth 2.0; mobile apps use the device's native contacts API via `flutter_contacts` or a similar package. Both paths share a critical privacy architecture: contacts must be SHA-256 hashed before touching the server, matched against pre-hashed profile fields, and deleted from the database within 24 hours of import. The consent modal and GDPR delete option are not optional polish — they are regulatory requirements in most jurisdictions.
What users consider table stakes in 2026
- Single visible button ('Find friends from your contacts') that triggers the import flow — not buried in settings
- A clear consent modal before any access is requested that lists exactly what data is accessed, how long it is stored, and that the user can delete it immediately
- Graceful decline path: pressing 'Decline' returns the user to their prior screen without importing anything
- Immediate display of matched users after import with a clear 'Add Friend' or 'Follow' action per match
- Unmatched contacts shown as 'Invite [Name]' rows with a one-tap email or SMS invite action
- A visible 'Delete my imported contacts' button in account settings that clears the import record immediately
Anatomy of the Feature
Seven components — AI tools handle the UI and Edge Function scaffolding reasonably well. The hashing layer, GDPR expiry cron, and Google OAuth redirect URI registration are where builds consistently fail without explicit instruction.
Google People API connector
ServiceA Supabase Edge Function handles the Google OAuth 2.0 code exchange: it receives the authorization code from the client, exchanges it with Google's token endpoint for an access token, then calls `https://people.googleapis.com/v1/people/me/connections?personFields=emailAddresses,phoneNumbers` to retrieve the contact list.
Note: The OAuth `redirect_uri` must be the published URL (not the Lovable preview or localhost) registered in Google Cloud Console under OAuth Credentials. The preview URL changes per session and causes 'redirect_uri_mismatch' errors every time.
Native contacts access (mobile)
ServiceOn Flutter/FlutterFlow, the `flutter_contacts` package reads device contacts as a list of `{ name, phones: [], emails: [] }` objects after `permission_handler` confirms the contacts permission is granted. On iOS, a usage description string in Info.plist is required or the App Store rejects the build.
Note: If the user previously denied contacts permission on iOS, `flutter_contacts` returns an empty array without throwing an error. Always check `PermissionStatus` first and, if denied, show a deep-link to iOS Settings rather than silently failing.
Contact hashing layer
BackendA Supabase Edge Function receives the raw contacts list from the client or Google People API response and hashes every email and phone number using SHA-256 via the Web Crypto API (`crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))`). Only the hex-encoded hashes are passed to the DB — raw emails and phone numbers never touch Supabase storage.
Note: Normalise values before hashing: lowercase all emails, strip spaces and non-digits from phone numbers, prefix with country code. Two users hashing '+1 (555) 555-0100' and '15555550100' must produce the same hash to match correctly.
Match display component
UIA shadcn/ui Avatar grid comparing the hashed imported contacts against the `hashed_email` and `hashed_phone` columns in `profiles`. Matched users are displayed with their avatar and name plus an 'Add Friend' or 'Follow' button. Unmatched contacts render as 'Invite [Name]' rows with one-tap email/SMS options.
Note: Run the match query server-side in the Edge Function, not client-side — sending the full list of hashed contacts to the frontend and filtering there leaks the hash list to the client.
Invite sender
ServiceA Supabase Edge Function calls Resend for email invites or Twilio's Messages API for SMS invites. Each invite message includes a unique referral token generated with `crypto.randomUUID()` stored in an `invite_tokens` table with a UNIQUE constraint on the token column.
Note: Never generate referral tokens with `Math.random()` — the collision probability is significant at scale. A UUID v4 has 2^122 possible values; `Math.random().toString(36)` has roughly 2^52.
Ephemeral contact store
DataThe `contact_imports` table stores only hashed contacts as a jsonb array, with `expires_at` set to `now() + interval '24 hours'` on insert. A pg_cron job (or scheduled Supabase Edge Function) deletes expired rows daily. Users can trigger immediate deletion from their settings.
Note: If pg_cron extension is not available on your Supabase plan, schedule a Supabase Edge Function via Supabase's built-in cron scheduler (Dashboard → Edge Functions → Schedule) to run `DELETE FROM contact_imports WHERE expires_at < now()` daily.
Permission and consent UI
UIA modal shown before any contact access is requested. Contains a bulleted list of exactly what is accessed (email addresses and phone numbers from Google Contacts or device), how it is stored (hashed, not readable), retention period (24 hours), and a link to the privacy policy. 'Allow' and 'Decline' buttons — decline returns the user to the previous screen with no side effects.
Note: Do not pre-check any consent boxes or default to Allow. GDPR Article 7 requires consent to be freely given, specific, and unambiguous — an auto-dismissed modal does not qualify.
The data model
This feature requires two schema additions: a `contact_imports` table for ephemeral hashed storage and `hashed_email`/`hashed_phone` columns on the existing `profiles` table. Run this in the Supabase SQL editor — pg_cron handles the 24-hour expiry automatically:
1-- Enable pg_cron extension (run once per project)2create extension if not exists pg_cron;34-- Add hashed fields to user profiles for contact matching5alter table public.profiles6 add column if not exists hashed_email text,7 add column if not exists hashed_phone text;89create index if not exists profiles_hashed_email_idx10 on public.profiles (hashed_email)11 where hashed_email is not null;1213create index if not exists profiles_hashed_phone_idx14 on public.profiles (hashed_phone)15 where hashed_phone is not null;1617-- Ephemeral contact import storage18create table public.contact_imports (19 id uuid primary key default gen_random_uuid(),20 user_id uuid not null references auth.users(id) on delete cascade,21 hashed_contacts jsonb not null default '[]',22 imported_at timestamptz not null default now(),23 expires_at timestamptz not null default (now() + interval '24 hours')24);2526alter table public.contact_imports enable row level security;2728-- Users can only access their own import rows29create policy "user owns import"30 on public.contact_imports for all31 using (auth.uid() = user_id)32 with check (auth.uid() = user_id);3334-- Invite tokens for referral tracking35create table public.invite_tokens (36 id uuid primary key default gen_random_uuid(),37 token text not null unique,38 inviter_id uuid not null references auth.users(id) on delete cascade,39 invitee_email text,40 invitee_phone text,41 created_at timestamptz not null default now(),42 accepted_at timestamptz43);4445alter table public.invite_tokens enable row level security;4647create policy "inviter can manage tokens"48 on public.invite_tokens for all49 using (auth.uid() = inviter_id)50 with check (auth.uid() = inviter_id);5152-- pg_cron: delete expired contact imports daily at 3am UTC53select cron.schedule(54 'delete-expired-contact-imports',55 '0 3 * * *',56 $$ delete from public.contact_imports where expires_at < now() $$57);Heads up: The pg_cron extension is available on Supabase Pro and above. On the free tier, schedule a Supabase Edge Function via the Dashboard cron scheduler instead. The hashed_email and hashed_phone fields on profiles should be populated on user signup by hashing the email in an Edge Function trigger — this enables instant matching without exposing plaintext values.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Best path for mobile apps — `flutter_contacts` and `permission_handler` provide native device contact access without needing Google OAuth. Firebase can store hashed contacts. Google People API requires a custom Dart action for the OAuth flow.
Step by step
- 1Add `flutter_contacts` and `permission_handler` in FlutterFlow Settings → Pub Dependencies
- 2Add a custom Dart action 'ImportDeviceContacts' that: checks ContactsPermission via permission_handler, shows the system permission dialog if not yet requested, reads all contacts as a list of { name, phones, emails }, hashes each value with the `crypto` Dart package using SHA-256, sends the hashed list to a Supabase Edge Function for matching
- 3In FlutterFlow Action editor, wire the 'Import Contacts' button to the custom action, then chain an API call to the Supabase match Edge Function, then navigate to the 'Results' page passing the matched/unmatched lists
- 4Add a consent bottom sheet page before the button triggers the action — include the required disclosures about data access and the 24-hour retention
- 5In iOS Settings → Permissions, add the NSContactsUsageDescription string: 'Used to find your friends who are already on [App Name]' — without this the App Store will reject the build
Where this path bites
- Google People API OAuth (for Google Contacts on web) requires a custom Dart action with a full OAuth code exchange flow — not available in FlutterFlow's visual action editor
- GDPR 24-hour expiry logic needs a scheduled Firebase Cloud Function or Supabase scheduled Edge Function — FlutterFlow cannot schedule background jobs natively
- The contact hashing must happen in the custom Dart action before data leaves the device — verify the hashed values, not raw contacts, are sent to the Edge Function
Third-party services you'll need
Four services are involved — Google People API is free within quotas, costs scale with SMS invite volume:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Google People API | Access Google Contacts with user OAuth consent using contacts.readonly scope | 180 requests per user per minute; no monthly billing | Free — quota increase available via Google Cloud Console request (approx) |
| Twilio SMS | Send invite SMS to unmatched contacts who are not yet on the platform | Trial credits only; production requires paid account | $0.0079/SMS outbound US (approx) |
| Resend | Send invite emails to unmatched contacts with referral token link | 3,000 emails/mo | Pro $20/mo: 50,000 emails (approx) |
| Supabase | Hashed contact storage, Edge Functions for OAuth/matching/invite, pg_cron for expiry | 500 MB DB, 500K Edge Function invocations/mo | Pro $25/mo: 8 GB DB, 2M Edge Function invocations/mo |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Google API free. Resend free. Supabase free tier. Minimal Twilio cost ($0–$5) if a handful of SMS invites are sent. Total is effectively $0–$5.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Google OAuth fails in Lovable preview with redirect_uri_mismatch
Symptom: Google Cloud Console OAuth 2.0 credentials require every redirect URI to be pre-registered. Lovable's preview URL is dynamically generated per session (`https://[hash].lovable.app`) — it is never the same URL twice and can never be pre-registered. Every OAuth attempt from the preview returns a 400 'redirect_uri_mismatch' error.
Fix: Register only the published Lovable URL (e.g., `https://myapp.lovable.app`) in Google Cloud Console under OAuth Credentials → Authorized Redirect URIs. Add a visible banner in the UI: 'Contact import must be tested on the published URL, not in the editor preview.' Test only after clicking Publish in Lovable.
Plaintext contacts stored in the database
Symptom: AI tools default to inserting the raw contact data returned by the Google People API directly into the `contact_imports` table. This stores real names, email addresses, and phone numbers of third parties who never consented to your app — a direct GDPR Article 5 violation and regulatory risk regardless of jurisdiction.
Fix: The hashing step must happen inside the Edge Function before any DB write — not in the client, not after the insert. Use `const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(normalised))` in the Deno Edge Function, convert the ArrayBuffer to a hex string, and verify no `body`, `email`, or `phone` fields appear in the DB record.
Imported contacts never deleted
Symptom: AI tools often generate the `contact_imports` table without the `expires_at` column or without setting up the pg_cron deletion job. Imported contact hashes accumulate indefinitely — potentially violating GDPR Article 5(1)(e) on storage limitation, and growing the database unboundedly.
Fix: Set `expires_at = now() + interval '24 hours'` on every INSERT into `contact_imports`. Add the pg_cron schedule from the af_data_model SQL above. Verify the cron job exists in Supabase Dashboard → Database → Cron Jobs. Add an immediate-delete option in user settings as a secondary safeguard.
Permission denied on iOS after previous denial
Symptom: On iOS, if a user previously tapped 'Don't Allow' on the contacts permission system dialog, `flutter_contacts` returns an empty array on subsequent calls without throwing an error or showing the system prompt again. The import appears to succeed with zero contacts found — no feedback to the user that permission was denied.
Fix: Before calling `flutter_contacts.getContacts()`, check `await Permission.contacts.status`. If the status is `PermissionStatus.permanentlyDenied`, do not attempt import. Instead, show a modal with a deep-link to iOS Settings: `openAppSettings()` from `permission_handler`. Never silently return an empty list.
Invite link token collision at scale
Symptom: AI tools frequently generate referral tokens using `Math.random().toString(36).substring(2)` or similar approaches. These produce approximately 8 characters of Base-36 (2^41 possibilities), with a collision probability exceeding 1% once the table contains ~100K tokens — well within reach for any app with viral growth.
Fix: Use `crypto.randomUUID()` in Deno Edge Functions or `crypto.randomUUID()` in Node.js for all referral token generation. This produces a UUIDv4 with 2^122 possible values. Add a `UNIQUE` constraint on the `token` column in `invite_tokens` so any collision (however unlikely) causes a detectable error rather than silent overwriting.
Best practices
Hash contacts at the Edge Function level before any database interaction — never pass raw phone numbers or emails to Supabase storage
Normalise before hashing: lowercase emails, strip all non-digit characters from phone numbers and prepend the country code — two representations of the same number must produce the same hash
Set `expires_at` on every contact import row and verify the pg_cron deletion job exists in the Supabase dashboard — omitting the expiry is a GDPR violation, not just a best practice
Register only the published URL (never the preview URL) in Google Cloud Console OAuth credentials — the preview URL changes per session
Generate referral tokens with `crypto.randomUUID()` and enforce a UNIQUE constraint on the token column — `Math.random()` tokens collide at scale
Always check `PermissionStatus.permanentlyDenied` before calling the native contacts API on iOS — a denied permission returns empty data silently without throwing
Run the contact match query server-side in the Edge Function — sending hashed contact lists to the client and filtering there exposes the hash list unnecessarily
Offer an immediate 'Delete my imported contacts' option in account settings as a GDPR right-to-erasure compliance mechanism
When You Need Custom Development
One-click import covering Google Contacts and native device address books satisfies most apps. Four scenarios require custom development:
- You need multi-provider import (Google Contacts + Microsoft Outlook + LinkedIn) with cross-provider de-duplication — each provider has a different OAuth flow, rate limit, and data schema
- You need SOC 2 Type II compliance for a B2B app where imported contacts include enterprise customer data — requires formal data processing agreements and audit logs for every import event
- You need real-time contact sync that automatically updates matches as new users join the platform, not just on manual import — requires a database trigger or background job on every new signup
- You need to handle 10M+ imported contacts with batch hashing and comparison at scale — Edge Function timeouts and Supabase query limits require a chunked queue architecture
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
Can I import Google Contacts into my Lovable app?
Yes, via a Supabase Edge Function that handles the OAuth 2.0 code exchange. The client initiates the Google OAuth flow with the `contacts.readonly` scope, Google redirects back to your published URL with an authorization code, the Edge Function exchanges the code for a token using your GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from Supabase Secrets, then fetches the contacts list. You cannot test this in the Lovable preview — register the published URL in Google Cloud Console and test only after publishing.
How do I let users find friends already on my app from their contacts?
Hash every imported email and phone number with SHA-256 in the Edge Function before touching the database. On signup, hash each new user's email and phone and store the hashes in the `profiles` table. During import, query `WHERE hashed_email = ANY(imported_hashes) OR hashed_phone = ANY(imported_hashes)`. Run this query server-side — never send the hash list to the frontend for client-side filtering. This way plaintext contacts never leave the user's device.
Is storing imported contacts a GDPR concern?
Yes, significantly. Contacts are personal data of third parties who have not consented to your app. GDPR requires a lawful basis for processing (legitimate interest is narrow here), purpose limitation, and storage minimisation. The compliant approach: hash contacts before storage, never store plaintext names or numbers, set an automatic 24-hour expiry on all import records, provide an immediate delete option, and disclose the retention period to the user before they initiate the import.
How do I send SMS invites to contacts who are not on the platform?
Use a Supabase Edge Function that calls Twilio's Messages API with the invitee's phone number and a message containing a unique referral link. Generate the referral token with `crypto.randomUUID()` (not Math.random) and store it in an `invite_tokens` table with a UNIQUE constraint. Twilio costs approximately $0.0079 per outbound US SMS. If cost is a concern, offer email-only invites via Resend (3,000 free/month) and make SMS opt-in for the sender.
Why does Google OAuth fail in the Lovable preview?
Lovable's preview URL is dynamically generated per session and cannot be pre-registered in Google Cloud Console. Google requires every OAuth redirect URI to be explicitly whitelisted — any non-registered URI returns a 400 'redirect_uri_mismatch' error. Register your published Lovable URL (e.g., myapp.lovable.app) in Google Cloud Console under OAuth 2.0 Credentials → Authorized Redirect URIs, then test only after publishing.
How do I access phone contacts in a FlutterFlow mobile app?
Add `flutter_contacts` and `permission_handler` as Pub dependencies in FlutterFlow Settings. Create a custom Dart action that: checks `await Permission.contacts.status`, shows the system permission dialog if not yet requested, reads contacts with `flutter_contacts.getContacts()`, hashes each phone number and email with SHA-256 using the Dart `crypto` package, then sends the hashes to a Supabase Edge Function for matching. If `PermissionStatus.permanentlyDenied`, show a modal linking to iOS Settings — never silently return empty results.
How long should I keep imported contact data?
24 hours is the industry standard for ephemeral contact matching. The import only needs to persist long enough to run the match query and send invites — after that, retaining the data creates GDPR exposure with no product benefit. Set `expires_at = now() + interval '24 hours'` on every INSERT and run a daily pg_cron job to delete expired rows. Also offer an immediate delete option in account settings for users who want to remove their data before the 24 hours elapse.
What permissions do I need to request from users before importing contacts?
For Google Contacts on web: the `https://www.googleapis.com/auth/contacts.readonly` OAuth scope — users will see a Google consent screen listing what the app can access. For native mobile: the device contacts permission (`NSContactsUsageDescription` on iOS, `READ_CONTACTS` on Android). In both cases, show your own consent modal before triggering the system permission request — explain what is imported, how it is stored (hashed only), and the 24-hour retention period. A clear Decline path is legally required under GDPR Article 7.
Need this feature production-ready?
RapidDev builds an one-click contact importer into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.