The Problem We Solve
Imagine you’re on a website and want to invite your friends to an event, share with your team, or tag people in a photo. The website could help you do that—but only if it gets access to all your contact information. Which means your phone’s address book, with your family’s phone numbers and your coworkers’ emails, gets uploaded to their servers.
That’s the tradeoff of the internet in 2024: social features require your private data.
Until now.
How Groups Changes the Game
Groups is a Safari browser extension that lets websites become social without ever seeing your contacts. Here’s what that actually means:
When a website uses Groups, it can show your contacts, match them against its own people, filter them by group (family, work, friends), and let you select who to invite—all while your actual contact information never leaves your phone.
Not “encrypted so we can’t read it.” Not “private policy pinky promise.” Never sent to the website’s servers in the first place.
The Architecture: Three Layers
Layer 1: URL and Token Matching (No Real Data Exchanged)
Instead of sending your contact’s actual name or email to a website, Groups uses identifiers that are meaningless outside your phone.
Here’s how it works:
-
Your phone stores contact URLs. Each contact has social profile URLs (LinkedIn, Facebook, etc.) and a local token—a random 8-character string. These are stored only on your phone.
-
The website stores URLs of its own users. If a team member registered with LinkedIn, the website has that URL. If someone signed up with email, it has that email address. If they joined via your referral, it has a reference URL.
-
Groups matches URLs locally. When you open the embed, Groups compares your contacts’ URLs to the website’s known identifiers. If there’s a match, it knows “this person in my phone is on that website”—without the website ever seeing your contact’s real data.
-
Only matches get revealed. The website only sees contacts you selected. It never learns which contacts didn’t match, and it never sees your full contact book.
-
On selection, a reference URL is created. When you invite someone, Groups generates or uses an existing URL for that person (e.g.,
https://example.com/abc12345). This is sent to the website as the contact identifier. On future visits to the same website, Groups uses the same URL, so the website can recognize you as the person who invited them.
Example: Your friend Alice has a LinkedIn profile at linkedin.com/in/alice-smith. The team app knows Alice’s LinkedIn URL because she logged in with it. Groups compares URLs locally and finds a match. When you click “Invite Alice,” the app receives https://example.com/xyz789—a reference that maps to Alice on that website only. The app never learns her name, email, or LinkedIn credentials.
Layer 2: The Browser Extension Proxy
Groups runs as a Safari extension on your phone (iOS 17+). This matters because:
- Your extension has permission to access your contacts. Safari extensions can read the native Contacts app.
- The website does not have permission to access your contacts. Websites run in a sandbox; they can’t ask iOS for contact data even if they wanted to.
- Groups is the intermediary. It reads your contacts, generates random tokens, does the matching, and returns only what you approve.
When a website wants to be social, it doesn’t embed a contact picker. Instead, it embeds a simple HTML form with a custom DSL (domain-specific language) that Groups interprets.
Layer 3: The Embed DSL (Domain-Specific Language)
Websites don’t write custom code to use Groups. Instead, they use a simple markup language that Groups understands.
For example, a website might include:
<div data-foreach-contact data-multiselectable>
<img data-contact-thumbnail-nobg src="fallback.png">
<span data-contact-firstname></span>
<span data-contact-lastname></span>
<a data-contact-email href="mailto:"></a>
</div>
<button data-submit>Invite Selected</button>
This tells Groups: “Loop through all my contacts that match your members. For each one, show their thumbnail, first name, last name, and email. Let me click to select multiple people. When I click submit, send the ones I picked.”
Groups interprets this markup in a special iframe, fills in the real data from your phone, and handles the selection. The website sees only the final selections.
Key security features of the DSL:
- No JavaScript execution. The markup is inert; Groups renders it safely.
- No external HTTP requests. Contact data never touches the network.
- No event handlers. The website can’t sneak in click listeners to spy on what you scroll past.
- Lazy image loading. Contact thumbnails are loaded locally from your phone, one at a time, only as they come into view. The fallback image is never fetched.
What Information Can the Website Learn?
What it CAN see:
- Contacts you explicitly selected for sharing
- Their reference URL on that website (e.g.,
https://example.com/xyz789) - Metadata you approved (first name, last name, email, phone)
What it CANNOT see:
- Your full contact list
- Contacts you didn’t select
- Private details (notes, birthday, home address)
- Any contacts that didn’t match its member database
- Which contacts you scrolled past, searched for, or considered but rejected
Three Matching Modes
1. Prefix Filtering (Same-Origin Only)
A website can ask: “Show me contacts whose profile URLs contain ‘linkedin.com’.”
This is safe because:
- Groups verifies the website’s origin first. A website can only filter by its own domain.
- Cross-origin filtering is blocked by default (can be overridden via privacy config, but users will be warned).
Use case: A professional network can show you colleagues from LinkedIn.
2. Group Tags
Groups uses standard categories: family, friends, work, neighbors.
A website can ask: “Show only contacts tagged as ‘work’.”
Your phone already categorizes your contacts (most phones do). Groups respects those categories without revealing which contacts are in each group unless you enable that filter.
Use case: An event planner showing only your friends, not your colleagues.
3. Search
You can type in a search box. Groups filters in real-time, showing only contacts whose names match.
The website sees: “User selected a contact named Alice.”
The website does not see: “User searched for ‘Alice,’ deleted the search, searched for ‘Bob,’ then deleted that too.”
Each filter (prefix, tag, search) runs locally on your phone. No queries are sent to the website.
Nested Loops and Smart Display
Groups supports nested loops. For example:
<div data-foreach-group>
<h3 data-group-name></h3>
<div data-foreach-contact>
<span data-contact-firstname></span>
</div>
</div>
This renders: “For each group (work, family, friends), show the group name, and underneath, show all contacts in that group.”
Groups handles the data joining locally. The website receives only a flat list of selections.
The Selection and Submission Flow
- User clicks a contact. The contact is marked as selected (visually highlighted).
- User clicks “Submit” or “Share.” Groups collects the selected contacts.
- Groups redirects to the callback URL with the selections as URL parameters:
https://example.com/invite?contacts[0]=contact-id-1&contacts[1]=contact-id-2&groups[0]=group-id-1
The callback URL is specified by the website (e.g., submitUrl parameter) and is verified to match the website’s origin.
No data is leaked during submission: Only the identifiers you selected are sent, and they’re sent over HTTPS to the destination the website declared.
Privacy-First Architecture Decisions
Hash Fragments, Not Query Strings
When a website embeds Groups, it passes its HTML template via the URL:
https://embed.groups.app/#html=<encoded HTML>
Notice the # (hash fragment). Here’s why that matters:
-
Query strings (
?html=...) are sent to the server. The server atembed.groups.appwould see all your contact data if it were passed as a query string. -
Hash fragments (
#html=...) are NOT sent to the server. They stay on your phone. Only your browser sees them.
So even if someone were wiretapping the network between your phone and the embed server, they’d see an empty URL with no contact data in it.
Lazy Image Loading
Contact photos (thumbnails and icons) are stored locally on your phone, indexed by a SHA-1 hash of the contact’s identifier.
When Groups needs to display a thumbnail:
- It computes the hash of the contact ID.
- It reads the image file from local storage (via the extension’s private file system).
- It converts it to a data URL and displays it.
The fallback image provided by the website is never fetched for contacts that have a photo on your phone. This prevents the website from learning which contacts matched (by observing which fallbacks are actually loaded).
Inert Rendering
The DSL markup is rendered as plain HTML, not executed as code. This means:
- No
<script>tags. Even if the website tries to embed JavaScript, it’s stripped out. - No event handlers like
onclickoronload. Websites can’t add spy code. - No
<iframe>tags. Websites can’t embed trackers. - No external images, stylesheets, or fonts (unless explicitly allowed).
The rendering engine is stateless and fast. It just walks the DOM tree, fills in your contact data, and returns inert HTML.
Real-World Examples
Example 1: Event Invitation
A wedding planning website wants to let you invite guests.
Without Groups:
- You upload your entire contact list.
- The website stores it.
- It might sell it to data brokers.
- Hackers might steal it.
With Groups:
- The website shows you its guest list (confidentially).
- Groups matches your contacts against it.
- You see only the people you know who haven’t RSVP’d.
- You click to select 5 friends to invite.
- The website gets their identifiers and names. That’s it.
Example 2: Team Collaboration
A project management tool wants to let you assign tasks to teammates.
Without Groups:
- You grant the app access to your phone’s directory.
- Your boss’s personal phone number is exposed to their servers.
- Your family contacts become known to HR.
With Groups:
- You tag your colleagues as “work” in your phone.
- Groups filters to show only work contacts.
- You assign tasks. The tool learns you’re working with Alice and Bob.
- It never learns your boss’s home phone or your partner’s email.
Example 3: Photo Sharing
A photo app wants to let you tag people in photos.
Without Groups:
- You upload your contact list to identify people.
- The app learns who all your friends are.
With Groups:
- You’re shown thumbnails of your contacts (from your phone, not the network).
- You click to tag faces.
- The app learns only the identities of people you actually tagged, not everyone you know.
Technical Details for the Curious
The Snapshot Data Structure
Groups maintains a “snapshot” of contact and group data:
{
contacts: { /* contact ID => contact object */ },
groups: { /* group ID => group object */ },
members: { /* group ID => { contact IDs } */ },
memberships: { /* contact ID => { group IDs } */ },
tokens: { /* token => contact ID */ }
}
This is computed once when the extension loads and updated when new contacts are detected. It never leaves your phone.
Prefix Matching
When a website asks to filter by prefix (e.g., “linkedin.com”), Groups:
- Checks if the website’s origin matches (or is in a trusted list).
- Looks at each contact’s URLs (stored locally).
- Checks if any URL contains the prefix.
- If yes, includes the contact in the filtered list.
All comparisons are case-insensitive and happen on your phone.
Nesting Depth Limits
For performance, Groups limits nested loops to 1 level deep. This prevents:
- Infinite loops.
- Rendering cascades that freeze the phone.
- Accidentally exposing too much data in overly complex templates.
Group Tagging and Colors
Each group can have a tag (family, friends, work, neighbors) and an auto-generated color based on the group’s name.
The color is deterministic (same name = same color every time), making groups visually consistent across websites.
What Groups Doesn’t Do (and Why That Matters)
- Groups doesn’t store your selections. Each time you use an embed, your choices are fresh. The website doesn’t learn your history of who you usually invite.
- Groups doesn’t create a unified profile. It doesn’t synthesize data about you from multiple websites.
- Groups doesn’t use AI to predict missing matches. It only shows exact matches, preventing guessing games about your social graph.
- Groups doesn’t charge websites per match. There’s no incentive to leak data about non-matches to improve their funnel.
The Future: More Websites, Better Defaults
Right now, Groups works with websites that explicitly integrate the DSL. But the architecture is designed to scale:
- More websites will adopt it as the social feature becomes standard.
- Better defaults will emerge. Over time, common UI patterns (inviting people, tagging contacts, sharing in groups) will become templates that websites can reuse.
- Interoperability will grow. Groups integrates with the browser’s native permission model. Other extensions and apps can do the same.
Why This Matters
The internet today treats your social graph like it treats your financial data: valuable, centralized, and routinely leaked.
Groups shows that doesn’t have to be true.
By moving the matching and rendering logic to your phone, Groups proves you can have a fully social web without centralizing your relationships. Websites get the features they want (contact integration, team management, event invitations). You keep control over your data.
The underlying insight: computation close to the data, not data sent to the computation.
And it works.
Advanced Features: Tags, Tokens, and Pristine Execution
Contact Tags
Every contact can be tagged with standard categories. Groups enforces a fixed set to prevent data leakage:
familyfriendsworkneighbors
When a website requests “show only work contacts,” Groups filters locally. The website never learns your tags, only which contacts you selected.
Contact Tokens and Identifiers
Groups assigns each contact multiple identifiers:
- Contact ID: Internal identifier for the contact in your phone.
- Token: A short random string (8 chars) generated per contact for internal use only. Never shared with websites. Useless outside your phone.
- Contact URLs: Links from social profiles, email addresses, or website registrations. Stored locally and used for matching.
-
Reference URL: A website-specific URL created when you invite someone (e.g.,
https://example.com/xyz789). This is what the website sees.
When you select a contact to invite, Groups generates or uses an existing reference URL for that website. The website receives this URL, not your contact’s email or phone.
The website can recognize that contact if they return to the same website, but it has no way to reverse-engineer who that is without the local context on your phone.
Pristine Execution Environment
The embed runs in a pristine iframe with zero external dependencies:
- No network requests (except the final callback).
- No cookies shared with parent window.
- No local storage access.
- No service workers inside the iframe.
- No external scripts—all code is baked into the extension.
- No trackers—the fallback image is never fetched if a local photo exists.
This means:
- The parent website can’t spy on what you scroll past.
- Third-party tracking pixels can’t build a profile of your actions.
- No data leaves your phone except what you explicitly approve.
Signing and Secure Enclave
For sensitive operations (like authorizing contact updates), Groups uses cryptographic signing:
On iOS, this happens in the Secure Enclave:
- A private key is stored in hardware isolation (Secure Enclave) on your phone.
- When a contact is shared or updated, the action is signed with that key.
- The signature proves the action came from your phone, not a website impersonating you.
- Websites can verify the signature using a public key.
Verification flow:
- Website receives a contact update with a signature.
- Website checks the signature against Groups’ published public key.
- If the signature is valid, the website trusts the update came from your phone.
- If the signature is forged or tampered, verification fails.
This prevents:
- A rogue website claiming you shared a contact when you didn’t.
- A man-in-the-middle modifying your contact data in transit.
- Impersonation of your identity to other websites.
The DSL: Complete Reference
Loop: Rendering Contact Lists
<div data-foreach-contact>
<div class="card">
<img data-contact-thumbnail-nobg src="fallback.png">
<span data-contact-firstname></span> <span data-contact-lastname></span>
<a data-contact-email href="mailto:"></a>
<a data-contact-phone href="tel:"></a>
</div>
</div>
Attributes:
-
data-foreach-contact: Loop over all contacts (or filtered set). -
data-contact-thumbnail-nobg: Load avatar from phone, no background removed. -
data-contact-icon-nobg: Smaller icon version. -
data-contact-firstname,data-contact-lastname: Name fields. -
data-contact-email,data-contact-phone: Contact methods (auto-formatted as mailto/tel).
Filtering the loop:
<!-- Search input (live filter) -->
<input id="search" type="text">
<div data-foreach-contact data-for="#search">...</div>
<!-- By tag (family, work, etc.) -->
<div data-foreach-contact data-filter-group="work">...</div>
<!-- By URL prefix (LinkedIn, Facebook, etc.) -->
<div data-foreach-contact data-filter-prefix="linkedin.com">...</div>
<!-- Limit results -->
<div data-foreach-contact data-limit="5">...</div>
Loop: Rendering Groups
<div data-foreach-group>
<h3 data-group-name></h3>
<span data-group-count></span> members
<div style="background: auto;" data-group-colors></div>
</div>
Attributes:
-
data-foreach-group: Loop over all groups. -
data-group-name: Group name (family, work, etc.). -
data-group-count: Number of members in group. -
data-group-colors: Auto-generated color from group name (deterministic).
Nested Loops
<div data-foreach-group>
<h3 data-group-name></h3>
<!-- Nested: show all contacts in this group -->
<div data-foreach-contact>
<span data-contact-firstname></span>
</div>
</div>
Nesting is limited to 1 level deep for performance and security reasons.
Selection and Submission
<!-- Single selection -->
<div data-foreach-contact data-selectable>
<img data-contact-thumbnail-nobg src="fallback.png">
<span data-contact-firstname></span>
</div>
<!-- Multi-select -->
<div data-foreach-contact data-multiselectable>
<img data-contact-thumbnail-nobg src="fallback.png">
<span data-contact-firstname></span>
</div>
<!-- Submit button -->
<button data-submit>Invite Selected</button>
Flow:
- User clicks contacts (visually highlighted with
.Groups_selectedclass). - User clicks “submit” button.
- Groups collects selected contact identifiers.
-
Redirect to callback URL:
https://example.com/invite?contacts[0]=contact-1&contacts[1]=contact-2
Contact Attributes (Single Contact)
If you want to display a specific contact (not a loop):
<div data-contact-url="https://groups.app/contact/alice-12345">
<img data-contact-thumbnail-nobg src="fallback.png">
<span data-contact-firstname></span>
<a data-contact-email href="mailto:"></a>
</div>
Or by token:
<div data-contact-token="abc12345">
<span data-contact-firstname></span>
</div>
If the contact is found locally, the fields are filled. If not found, they remain empty.
Group Picker (Dropdown)
<select data-group-picker>
<option>All Contacts</option>
</select>
<!-- This dropdown updates what this loop shows -->
<div data-foreach-contact data-for="select[data-group-picker]">
<span data-contact-firstname></span>
</div>
When the user selects a group, the contact loop updates in real-time.
Iframe Integration
Groups works beautifully with iframes. A website can embed arbitrary HTML inside an iframe, and Groups fills in contact data.
Pattern 1: Embed an Iframe with HTML Template
// Parent window (your website)
const html = `
<div data-foreach-contact data-multiselectable>
<img data-contact-thumbnail-nobg src="fallback.png">
<span data-contact-firstname></span>
</div>
<button data-submit>Invite</button>
`;
const encoded = encodeURIComponent(html);
const iframeSrc = `https://embed.groups.app/#html=${encoded}&submitUrl=${encodeURIComponent('https://example.com/invited')}`;
const iframe = document.createElement('iframe');
iframe.src = iframeSrc;
document.body.appendChild(iframe);
When the user submits, they’re redirected to https://example.com/invited?contacts[0]=...&contacts[1]=...
Pattern 2: Communicate via PostMessage
For more control, use postMessage instead of redirect:
// Parent window listens for submissions
window.addEventListener('message', (event) => {
if (event.origin !== 'https://embed.groups.app') return;
if (event.data.contacts) {
// User submitted these contacts
console.log('Selected:', event.data.contacts);
// Handle the selection without page reload
}
});
// Inside the iframe (via postMessage API)
// Groups sends: { contacts: ['url1', 'url2'], echo: 'some-id' }
Pattern 3: Server-to-Server Verification
After the user submits, verify the selection server-side:
// Backend (Node.js / PHP / etc.)
const payload = {
contacts: ['contact-token-1', 'contact-token-2'],
deviceId: 'user-device-id',
timestamp: Date.now()
};
// Verify signature from secure enclave
const isValid = await verifySignature(payload, publicKey, signature);
if (isValid) {
// Trust this contact list came from the user's phone
addContactsToGroup(userId, payload.contacts);
}
Real-World Use Cases
1. Event Invitation
Website: Event management platform (Eventbrite-like)
Flow:
- User lands on event page.
- Embed loads: “Invite your friends to this event”
- Groups shows contacts who are already registered on Eventbrite.
- User selects 5 friends.
- Submit → contact identifiers sent to event server.
- Server adds them to the event’s invited list.
Privacy: Event platform never sees your full contact list, only identifiers of people you explicitly invited.
2. Team Collaboration
Website: Project management tool (Asana-like)
Flow:
- User creates a new project.
- Embed loads: “Add team members”
- Groups filters to show only “work” contacts.
- User selects teammates.
- Submit → server adds them to project.
Privacy: Boss doesn’t see your personal contacts, kids’ friends, or casual acquaintances.
3. Photo Tagging
Website: Photo app (Instagram-like)
Flow:
- User uploads a photo.
- Embed loads: “Tag people in this photo”
- User clicks faces or search for people.
- Groups shows matching contacts.
- Submit → server tags the photo.
Privacy: Photo app learns only who you tagged, not your entire contact graph.
4. Group Messaging
Website: Messaging app
Flow:
- User creates a new group chat.
- Embed loads with group picker: “Choose a group or add people”
- User selects “family” group.
- Submit → server creates group chat with those contacts.
Privacy: Messaging app never knows you have a “family” group—only that you selected people in it.
5. Referral Program
Website: SaaS product (Slack-like)
Flow:
- User navigates to “Refer Friends”
- Embed loads: “Who should we invite?”
- Groups shows contacts who don’t have an account yet.
- User selects 10 people to refer.
- Submit → server sends referral emails (signed, verified).
Privacy: Product doesn’t see your non-user contacts, only referral results.
How Tokens Work: A Deep Dive
Token Generation
When you first use Groups, the extension generates a random 8-character token for each contact:
const token = generateRandomId(); // e.g., "xyzabc12"
Groups.Contacts.byToken[token] = contactId;
localStorage['Groups_contacts_tokens'] = JSON.stringify({...});
Reference URL Mapping
The extension maintains local and reference URL maps:
Local (never leaves phone):
{
"alice-contact-id": {
"token": "xyzabc12", // internal token
"linkedin.com": "linkedin.com/in/alice-smith",
"example.com": "https://example.com/ref-123" // reference URL for this site
}
}
Sent to websites:
URL: https://example.com/invite?contacts[0]=https://example.com/ref-123&contacts[1]=https://example.com/ref-456
Website Perspective
A website receives a reference URL like https://example.com/ref-123. It doesn’t know who that is without your phone’s local mapping. It can:
- Store it as a reference for this user on its platform.
- Send it back if it needs to contact the person again.
- Recognize the URL if you invite the same person later.
But it can’t:
- Reverse-engineer the person’s name, email, or phone.
- Use the URL on another website (the URL is meaningless outside this site).
- Sell the URL to a data broker (worthless without the local mapping on your phone).
Reference URL Persistence
Reference URLs persist across browser sessions. If you invite https://example.com/ref-123 to an event today and join the same website next month, Groups uses the same reference URL.
This means the website can recognize you as the person who invited them, without ever seeing your contact’s real identity.
Updates: Applying New Contact Data
After sharing contacts with websites, they might send back updated information about those contacts:
Example: A social network updates a profile picture.
Flow:
- Website sends:
{ referenceUrl: "https://example.com/ref-123", updates: { profilePicture: "..." } } - Groups verifies the update is signed (secure enclave).
- If valid, Groups applies the update locally to the contact mapped to that reference URL.
- Next time you see that contact in Groups, their photo is current.
Updates are cryptographically signed to prevent:
- A rogue server sending fake updates.
- Man-in-the-middle tampering with data.
- Impersonation attacks.
Snapshot and Persistence
Groups maintains a snapshot of all your contacts and groups:
{
"version": 1,
"timestamp": 1718000000000,
"contacts": { "id1": {...}, "id2": {...} },
"groups": { "group1": {...}, "group2": {...} },
"members": { "group1": {"id1": true, "id2": true}, ... },
"memberships": { "id1": {"group1": true}, "id2": {"group1": true, "group2": true} },
"tokens": { "id1": ["abc123", "def456"], ... }
}
This snapshot is:
- Persisted to disk (in app-group container, shared with Share Extension).
- Never uploaded to the server.
- Loaded on app start to avoid refetching contacts.
- Kept in sync with native Contacts app and Groups’ own updates.
Architecture: The Three Layers (Revisited)
Layer 1: Native Code (Secure Enclave, Signing)
What runs here:
- Private key operations (Secure Enclave).
- Contact fetching from iOS Contacts app.
- Signature creation and verification.
- File I/O (reading/writing snapshots).
What it never does:
- Send contact data to the network.
- Access websites directly.
- Make decisions about what to share.
Layer 2: Browser Extension (Safari Web Extension)
What runs here:
- Message routing (native ↔ web bridge).
- Snapshot loading and merging.
- Popup UI (if user taps the extension).
- Embed module fetching and verification.
What it never does:
- Execute untrusted JavaScript.
- Store contact data in IndexedDB (only metadata).
- Make HTTP requests on behalf of websites.
Layer 3: Embed Environment (Isolated Iframe)
What runs here:
- DSL parsing and rendering.
- Contact field filling (from local snapshot).
- Event handlers (click, submit).
- Image loading (lazy, from local files).
What it never does:
- Access the parent window.
- Make network requests (except final callback).
- Store cookies or cache.
- Call native APIs directly.
Security Model: What Websites Can and Cannot Do
| What websites can do | Why it’s safe |
|---|---|
| Ask “show me contacts with emails” | Only filters locally; website doesn’t learn tags. |
| Suggest actions (“invite to event”) | You must click; no automation. |
| Store your token for later | Token is useless without the local mapping on your phone. |
| Verify signatures via public key | Signature proves the data came from your phone, not them. |
| Listen for referral submissions | You consciously chose who to refer. |
| What websites cannot do | Why blocked |
|---|---|
| Read your full contact list | Only see contacts you selected. |
| Infer which contacts didn’t match | Unmatched contacts are never shown. |
| Observe what you scrolled past | Filtration happens locally; no tracking. |
| Download photos of contacts | Photos are embedded as data URLs, never fetched from web. |
| Impersonate you to other websites | Signatures require your phone’s private key. |
| Update contacts without your permission | Updates require cryptographic signature verification. |
| Bypass the embed UI | All data flows through the DSL parser, which is sandboxed. |
The Result: A Fundamentally Different Web
With Groups, websites regain social features that used to require full contact list access:
- Event apps can show you which friends are attending.
- Team apps can auto-add your coworkers.
- Photo apps can tag you and your friends.
- Referral programs can target the right people.
But they do all this without ever seeing your address book.
Your contacts stay yours. Your privacy stays intact. The web stays social.
For Developers: Integrating Groups
If you run a website and want to add Groups:
- Design your HTML template using the DSL (data-foreach-contact, data-group-name, etc.).
-
URL-encode it and embed it in an iframe:
<iframe src="https://embed.groups.app/#html=...&submitUrl=..."></iframe> -
Listen for the callback. When users submit, they’re redirected to your
submitUrlwith their selections as URL parameters:?contacts[0]=...&contacts[1]=... - Verify signatures server-side (optional, but recommended for sensitive operations).
- That’s it. You don’t manage contact data, authentication, or privacy compliance. Groups does.
Integration Checklist
- [ ] Render Groups embed with your HTML template
- [ ] Handle the callback URL and store contact identifiers
- [ ] (Optional) Verify cryptographic signatures from your backend
- [ ] Test with multiple filtering modes (search, tags, prefixes)
- [ ] Monitor performance (lazy image loading should be fast)
Conclusion
Groups uses a different architecture: computation close to the data, not data sent to the computation.
The phone holds the master copy of contacts. Websites get filtered, anonymized references only. Servers never touch raw contact data. Users stay in control.
Your contacts. Your groups. Your phone. Your rules.
The social web doesn’t require surveillance. Groups proves it.
Want to try it yourself?
-
Head to something like https://hebrews.app/moo.html