A privacy-first, self-hosted contact manager for VCF and CSV exports from Apple Contacts, Google Contacts, and Microsoft Outlook. Parses, normalises, and renders contact libraries locally. No uploads. No third-party APIs. No build toolchain.
All contact data is processed in the browser. Nothing leaves the machine.
Contact exports from Apple iOS, Google Contacts, and Outlook all produce fundamentally incompatible files. Apple VCF uses X-ABLabel grouping and quoted-printable encoding for non-ASCII names. Google exports multi-value fields with ::: delimiters. Outlook CSV uses different column headers for the same data.
Contacts Hub reads all of them.
Core capabilities:
- Multi-library management. A PHP endpoint auto-discovers every
.vcfand.csvfile on the server at boot. Switch between libraries instantly from the sidebar. - Cross-format parsing. Dedicated parsers for VCF 3.0 and RFC 4180 CSV normalise both into a unified display schema. Apple, Google, and Outlook formats are handled without manual configuration.
- Full-text search. A fuzzy subsequence search runs across name, organisation, phone numbers, and email addresses simultaneously.
- Phone normalisation. A normalisation pipeline strips international dialling prefixes to derive canonical comparison keys, enabling reliable duplicate detection across contacts stored in mixed formats (e.g.,
+971507306079and00971507306079resolve to the same key). - Duplicate detection. A per-library cross-reference pass identifies contacts sharing any normalised phone key and flags them with inline indicators.
- In-browser CRUD. Add, edit, and delete contacts directly in the UI. Every mutation serialises the entire library back to VCF and persists it to the server via
write.php. Restricted to VCF libraries. - CSV-to-VCF conversion. A "Convert to VCF" action on any CSV library creates a new, independently editable VCF file on the server. A selective import modal allows merging new contacts from a CSV source into an existing VCF library, with phone-key-based deduplication.
- Full library export. Download any library as a single compiled VCF file. Available for both VCF and CSV libraries.
- My Card pinning. Set
MY_NAMEinconfig.jsto pin your own contact card to the top of every library list, surface a "My Card" badge in the detail pane in place of Edit and Delete controls, and personalise the share sheet text. Disabled whenMY_NAMEis empty. - Single-contact sharing. Generate a per-contact VCF blob and share it via the Web Share API (mobile) or download it directly (desktop).
- Token authentication. Both PHP endpoints require a matching
X-Nexus-Tokenheader. Unauthenticated requests receive HTTP 403. - Zero dependencies. No npm, no build step, no framework. One HTML file, one CSS file, one JavaScript module, two PHP scripts, one config file.
flowchart TB
subgraph Browser ["Browser — Vanilla JavaScript (ES Module)"]
config["config.js\nSCAN_TOKEN · MY_NAME"]
app["app.js\nVCF Parser · CSV Parser · Normalisation Engine\nDuplicate Detector · CRUD · CSV Converter\nSearch · Export · Sharing"]
ui["index.html + style.css\nWebOS SPA Shell · Tokyo Night · Responsive 3-Pane Layout"]
config -->|import| app
end
subgraph Server ["PHP Server (local or hosted)"]
scan["scan.php\nFile Discovery · Token Authentication Guard"]
write["write.php\nVCF Write · Token Guard · Path Traversal Guard"]
files["Contact Files\n*.vcf · *.csv"]
end
app -->|"GET scan.php\nX-Nexus-Token header"| scan
scan -->|"JSON [filenames]"| app
app -->|"GET *.vcf / *.csv\nX-Nexus-Token header"| files
files -->|"Raw file content"| app
app -->|"POST write.php\n{action, filename, vcfContent}"| write
write -->|"Writes *.vcf to filesystem"| files
At boot, window.onload calls scan.php to retrieve the file list, then issues parallel fetch() calls for every discovered file. Each file is parsed immediately and stored in libraries[]. recomputeDuplicates() runs on every library before the sidebar renders.
Full architecture reference: docs/ARCHITECTURE.md
| Component | Minimum |
|---|---|
| PHP | 8.0 |
| Browser | Chrome 90+, Firefox 90+, Safari 15+, Edge 90+ |
| Contact exports | Apple VCF, Google Contacts CSV or VCF, Outlook CSV or VCF |
No other dependencies.
1. Clone the repository.
git clone https://github.com/nshah1d/contacts-hub.git
cd contacts-hub2. Add contact files.
Place .vcf or .csv export files directly in the project root. The application discovers all files matching *.{vcf,VCF,csv,CSV} at boot.
contacts-hub/
├── app.js
├── config.js
├── index.html
├── scan.php
├── style.css
├── write.php
│
├── iCloud_Export.vcf ← Apple Contacts export
├── Google_Contacts.csv ← Google Contacts CSV export
└── Outlook_Export.csv ← Outlook CSV export
3. Generate a token.
The token is a shared secret used by both config.js and the PHP endpoints. Any random hex string of at least 32 characters is appropriate.
# Linux / macOS
openssl rand -hex 32# Windows (PowerShell)
$b = New-Object byte[] 32
[System.Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($b)
[System.BitConverter]::ToString($b).Replace('-','').ToLower()4. Configure the application.
Open config.js and set both values:
export const MY_NAME = 'Your Name';
export const SCAN_TOKEN = 'your-generated-token';Open scan.php and set the matching constant on line 2:
define('EXPECTED_TOKEN', 'your-generated-token');Open write.php and set the same constant on line 2:
define('EXPECTED_TOKEN', 'your-generated-token');All three token values must be identical. MY_NAME activates the My Card feature: when set, the application locates the contact whose display name matches (case-insensitive) and pins it to the top of every library list with a distinct visual indicator, replaces the Edit and Delete controls in the detail pane with a "My Card" badge, and personalises the share sheet text. Leave MY_NAME empty to disable the feature entirely.
5. Start a local server.
php -S localhost:80006. Open the application.
Navigate to http://localhost:8000 in any modern browser.
contacts-hub/
│
├── app.js # All client-side logic: parsers, CRUD, duplicate detection,
│ # CSV conversion, export, sharing, search, UI rendering
├── config.js # User configuration: MY_NAME, SCAN_TOKEN
├── index.html # SPA shell: 3-pane layout, edit/add/import modals, SEO metadata
├── scan.php # PHP backend: file discovery, token authentication
├── style.css # Full design system: WebOS aesthetic, Tokyo Night palette,
│ # responsive breakpoints
├── write.php # PHP backend: VCF write, token guard, path traversal guard
├── robots.txt # Disallows all crawling of hosted instances
│
├── docs/
│ ├── ARCHITECTURE.md # Technical reference: parsers, normalisation, CRUD, CSV tools
│ └── CONFIGURATION.md # Full configuration and deployment reference
│
├── SECURITY.md
└── LICENSE
Apple VCF exports use X-ABLabel grouping for custom phone and email labels, and QUOTED-PRINTABLE encoding for contact names containing non-ASCII characters (Arabic, accented Latin, CJK). The VCF parser handles both:
- Line unfolding: CRLF-space and LF-space continuations are collapsed before parsing.
X-ABLabelresolution: group prefix (item1.,item2.) is matched to the label value and attached to the correspondingTELorEMAILentry.- Quoted-printable decoding:
=XXsequences are converted to%XXURI encoding and resolved bydecodeURIComponent(), correctly handling multi-byte UTF-8 sequences.
Google CSV exports use ::: as a multi-value separator within a single cell (e.g., multiple phone numbers in one column). The CSV parser splits on ::: after the RFC 4180 cell boundary is resolved. Google VCF exports are handled by the standard VCF parser.
Outlook CSV exports use distinct column header names (First Name, Last Name, Business Phone, etc.). The column detection uses candidate-list matching against known Outlook, Google, and generic header variants. Outlook VCF exports may use QUOTED-PRINTABLE encoding; the same decoding pipeline applies.
Full reference: docs/CONFIGURATION.md
| Variable | File | Description |
|---|---|---|
MY_NAME |
config.js |
Your display name. Activates My Card pinning when set. Leave empty to disable. |
SCAN_TOKEN |
config.js |
Token sent as X-Nexus-Token header on all fetch() calls. |
EXPECTED_TOKEN |
scan.php |
Server-side token for the file discovery endpoint. Must match SCAN_TOKEN. |
EXPECTED_TOKEN |
write.php |
Server-side token for the VCF write endpoint. Must match SCAN_TOKEN. |
Both PHP files ship with empty token constants. Set all three to the same non-empty value before use.
For local use, the PHP built-in server (php -S localhost:8000) is sufficient.
For a hosted deployment on any PHP-enabled web server:
- Upload
app.js,config.js,index.html,robots.txt,scan.php,style.css, andwrite.phpto a directory on the server. - Place contact export files (
.vcf,.csv) in the same directory. - Set
EXPECTED_TOKENin bothscan.phpandwrite.php, andSCAN_TOKENinconfig.js, to matching non-empty values. - Update the seven metadata fields in
index.html(og:url,og:image,og:site_name,twitter:url,twitter:image,link rel="icon",link rel="apple-touch-icon") to the live deployment URL. - Access the directory via HTTPS.
Full deployment reference: docs/CONFIGURATION.md
Full reference: SECURITY.md
- All contact parsing is client-side.
scan.phpreturns only filenames.write.phpaccepts only a filename and VCF content string. Neither endpoint reads or transmits contact field data. - Every request to both PHP endpoints requires a valid
X-Nexus-Tokenheader. Absent or mismatched tokens receive HTTP 403 before any filesystem operation runs. write.phpenforcesbasename()on the submitted filename and rejects any value whose extension is not.vcf, preventing path traversal and non-VCF writes.robots.txtships withDisallow: /to prevent search engines from indexing hosted instances.- No analytics, no telemetry, no external scripts beyond the Google Fonts CDN.
Licensed under the MIT Licence.