LifeVis Add-on Developer Guide

Build custom views for LifeVis using HTML, CSS, and JavaScript.

1. Overview

A LifeVis add-on is a single HTML file (with inline CSS and JS) that renders inside a sandboxed WebView. Your add-on gets read-only access to the user's data through window.lifevis — a promise-based JavaScript API injected automatically by the app.

Add-ons can:

Security: Add-ons run in a strict sandbox. All network requests are blocked by Content Security Policy. Add-ons cannot access the filesystem, make HTTP requests, or load external scripts. Data access is strictly read-only.

2. Quick Start

Here's an example add-on that demonstrates every API function. Copy this into the test harness to try it out.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy"
      content="default-src 'none'; script-src 'unsafe-inline';
              style-src 'unsafe-inline'; img-src data: blob:; font-src data:;">
<title>API Demo</title>
<style>
  body { font-family: system-ui; padding: 20px; }
  .card { display: flex; gap: 12px; margin-bottom: 16px; cursor: pointer; }
  .card img { width: 60px; height: 60px; object-fit: cover; border-radius: 8px; }
  .title { font-weight: 600; }
  .meta { color: #888; font-size: 0.85em; }
  .section { margin-top: 24px; }
  .section h2 { font-size: 1.1em; margin-bottom: 8px; }
  .tag { display: inline-block; background: #eef; padding: 2px 8px;
         border-radius: 12px; font-size: 0.8em; margin: 2px; cursor: pointer; }
  .city { display: inline-block; background: #efe; padding: 2px 8px;
          border-radius: 12px; font-size: 0.8em; margin: 2px; }
</style>
</head>
<body>
<div id="app">Loading...</div>
<script>
async function init() {
    const app = document.getElementById('app');

    // ── getPosts: fetch posts filtered by type, sorted by date ──
    const posts = await lifevis.getPosts({
        sortBy: 'date',
        sortOrder: 'desc'
    });

    // ── getTheme: adapt to light/dark mode ──
    const theme = await lifevis.getTheme();
    if (theme.isDark) document.body.style.background = '#1a1a2e';
    if (theme.isDark) document.body.style.color = '#e0e0e0';

    // ── getConfig / setConfig: persist add-on preferences ──
    let config = await lifevis.getConfig();
    config.viewCount = (config.viewCount || 0) + 1;
    await lifevis.setConfig(config);

    // ── Render posts with thumbnails ──
    let html = '<h2>Recent Posts (' + posts.length + ')</h2>';
    for (const post of posts.slice(0, 5)) {

        // ── getThumbnail: fetch image as data URL by post ID ──
        const thumb = await lifevis.getThumbnail(post.id);
        const imgTag = thumb.data
            ? '<img src="' + thumb.data + '">'
            : '';

        // ── viewPost: tap a post to open the native detail view ──
        html += '<div class="card" onclick="lifevis.viewPost(\'' + post.id + '\')">'
            + imgTag
            + '<div><div class="title">' + post.title + '</div>'
            + '<div class="meta">' + post.type
            + (post.cities && post.cities[0] ? ' &middot; ' + post.cities[0] : '')
            + '</div></div></div>';
    }

    // ── getTags: fetch all tags, optionally filter to skills only ──
    const tags = await lifevis.getTags();
    html += '<div class="section"><h2>Tags (' + tags.length + ')</h2>';
    tags.slice(0, 15).forEach(function(tag) {
        // ── editTag: tap a tag to open the native tag editor ──
        html += '<span class="tag" onclick="lifevis.editTag(\'' + tag.id + '\')">'
            + tag.name + '</span>';
    });
    html += '</div>';

    // ── getCities: fetch all cities with coordinates ──
    const cities = await lifevis.getCities();
    html += '<div class="section"><h2>Cities (' + cities.length + ')</h2>';
    cities.slice(0, 10).forEach(function(city) {
        // ── switchView: navigate to a built-in tab ──
        html += '<span class="city" onclick="lifevis.switchView(\'map\')">' + city.name + '</span>';
    });
    html += '</div>';

    // ── getEnabledViews: check which built-in tabs are active ──
    const views = await lifevis.getEnabledViews();
    html += '<div class="section"><h2>Enabled Views</h2>'
        + '<div class="meta">' + views.join(', ') + '</div></div>';

    html += '<div class="meta" style="margin-top:16px">View count: '
        + config.viewCount + '</div>';

    app.innerHTML = html;

    // ── onThemeChange: react when user toggles light/dark mode ──
    lifevis.onThemeChange(function(t) {
        document.body.style.background = t.isDark ? '#1a1a2e' : '#fff';
        document.body.style.color = t.isDark ? '#e0e0e0' : '#1a1a2e';
    });

    // ── onDataChange: refresh when user edits/imports data ──
    lifevis.onDataChange(function() { init(); });

    // ── onShowSettings: open your settings UI when toolbar button tapped ──
    lifevis.onShowSettings(function() {
        alert('Settings panel would open here');
    });
}

// Wait for the bridge, then initialize
if (window.lifevis) init();
else document.addEventListener('DOMContentLoaded', function() {
    var check = setInterval(function() {
        if (window.lifevis) { clearInterval(check); init(); }
    }, 50);
});
</script>
</body>
</html>

This example uses every API function. The CSP must include img-src data: blob: for thumbnails. See the full API reference below for details on each function.

3. Add-on Manifest

Each add-on has a manifest.json that describes it in the catalog and on disk after install:

{
    "id": "com.yourname.myplugin",
    "name": "My Plugin",
    "icon": "chart.bar.fill",
    "androidIcon": "bar_chart",
    "version": "1.0",
    "entryPoint": "MyPlugin",
    "author": "Your Name",
    "description": "A brief description of what your add-on shows."
}
FieldTypeRequiredDescription
idStringYesReverse-domain ID (must be unique)
nameStringYesDisplay name shown in the tab bar and store
iconStringYesSF Symbol name for the tab icon (iOS/macOS)
androidIconStringNoMaterial Icon name for the tab icon (Android). Falls back to a generic icon if omitted.
versionStringYesSemver version string
entryPointStringYesHTML filename without extension (e.g. "MyPlugin" loads MyPlugin.html)
authorStringYesDeveloper or company name
descriptionStringYesShort description for the store listing

Common Icon Mappings

LifeVis runs on iOS/macOS (SF Symbols) and Android (Material Icons). Here are some common equivalents:

Purposeicon (SF Symbol)androidIcon (Material)
Documentdoc.textdescription
Calendarcalendarcalendar_month
Chart (bar)chart.bar.fillbar_chart
Chart (pie)chart.piepie_chart
Mapmapmap
Personpersonperson
Tagtaglabel
Photophotophoto
Listlist.bulletformat_list_bulleted
Clock/Historyclockschedule

4. HTML Template

Every add-on HTML file must include the Content Security Policy meta tag. This is also enforced natively, but including it in your HTML ensures consistent behavior during development.

<meta http-equiv="Content-Security-Policy"
      content="default-src 'none'; script-src 'unsafe-inline';
              style-src 'unsafe-inline'; img-src data: blob:; font-src data:;">

This means:

Bundle size limit: The total extracted size of your add-on bundle must be under 5 MB. This is enforced during install. Add-on bundles should typically be well under 1 MB — they're just HTML, CSS, and JS.

5. Bridge API Reference

The bridge is available at window.lifevis. All data methods return Promises.

Data Access Read-Only

MethodReturnsDescription
getPosts(options?) Post[] Get posts. Options: { type, types, sortBy, sortOrder }
getTags(options?) Tag[] Get tags/skills. Options: { isSkill, category }
getCities() City[] Get all cities with coordinates
getTheme() { isDark } Get current theme mode
getThumbnail(postId) { data } Get base64 thumbnail. Returns { data: "data:image/..." } or { data: null }

getPosts() Options

OptionTypeDescription
typeStringFilter by single type: "post", "job", "education", "trip", "residence", "project"
typesString[]Filter by multiple types
sortByString"date" (default) or "startDate"
sortOrderString"desc" (default) or "asc"

getTags() Options

OptionTypeDescription
isSkillBooleanIf true, only return skills (not regular tags)
categoryStringFilter by skill category (e.g. "Programming", "Languages")

Native Actions Action

MethodDescription
viewPost(id)Open the native post detail sheet
editPost(id)Open the native post editor
editTag(id)Open the native tag/skill editor

Pass the post or tag id string (UUID) received from getPosts() or getTags().

// Make a job entry clickable
element.onclick = () => lifevis.viewPost(job.id);

Config Read-Only Action

MethodDescription
getConfig()Get this add-on's stored config object
setConfig(obj)Save config (replaces entire object)

6. Data Types

Post

{
    id: "uuid-string",
    title: "Post title",
    type: "job",              // post, job, education, trip, residence, project
    date: "2024-01-15T00:00:00Z",
    imageCount: 2,

    // Optional fields (present when applicable):
    startDate: "2022-06-01T00:00:00Z",
    endDate: "2024-01-15T00:00:00Z",
    company: "Acme Corp",     // jobs
    position: "Engineer",     // jobs
    school: "MIT",            // education
    degree: "BS",             // education
    fieldOfStudy: "CS",       // education
    content: "<p>HTML...</p>",
    source: "linkedin",
    cities: ["San Francisco", "New York"],
    tags: ["Python", "AWS"]
}

Tag

{
    id: "uuid-string",
    name: "Python",
    postCount: 12,
    isSkill: true,
    level: 8,                 // 1-10 proficiency
    category: "Programming",  // optional
    proficiency: "Expert",    // optional display label
    description: "..."        // optional skill description
}

City

{
    id: "uuid-string",
    name: "San Francisco",
    latitude: 37.7749,
    longitude: -122.4194,
    postCount: 45
}

7. Events

Register callbacks to respond to app events:

MethodCallbackDescription
onThemeChange(cb)cb(isDark)Light/dark mode changed
onConfig(cb)cb(configObj)Stored config delivered on load
onDataChange(cb)cb()User's data changed (after import or edit)
onShowSettings(cb)cb()User tapped the settings button in the toolbar
// Re-render when the user imports new data
lifevis.onDataChange(function() {
    init(); // re-fetch and redraw
});

// Respond to theme changes
lifevis.onThemeChange(function(isDark) {
    document.documentElement.classList.toggle('dark', isDark);
});

8. Config Persistence

Add-ons can save settings that persist across app launches. Config is a plain JSON object, scoped to your add-on.

// Save user preferences
await lifevis.setConfig({
    showPhotos: true,
    sortOrder: 'date',
    accentColor: '#2563eb'
});

// Load on startup
lifevis.onConfig(function(config) {
    // config = { showPhotos: true, sortOrder: 'date', ... }
    applySettings(config);
});

// Or fetch at any time
const config = await lifevis.getConfig();
setConfig() replaces the entire config object. To update a single field, read the current config first, merge, then save.

9. Settings UI

If your add-on has user-configurable options, implement a settings panel that opens when the user taps the toolbar settings button. This is typically a drawer or modal rendered in your HTML.

lifevis.onShowSettings(function() {
    // Show your settings drawer/modal
    document.getElementById('settings-drawer').classList.add('open');
});

// When the user changes a setting:
function onSettingChanged() {
    const newConfig = gatherSettings();
    lifevis.setConfig(newConfig);
    render(); // re-render with new settings
}

The app provides a gear icon in the toolbar automatically. When tapped, it calls your onShowSettings callback.

10. Theme Support

Add-ons should support both light and dark mode. The recommended approach is CSS custom properties with a .dark class:

:root {
    --bg: #ffffff;
    --text: #1a1a1a;
    --accent: #2563eb;
}
.dark {
    --bg: #161618;
    --text: #f0f0f0;
    --accent: #60a5fa;
}
body { background: var(--bg); color: var(--text); }
// Apply theme on load
lifevis.getTheme().then(function(t) {
    document.documentElement.classList.toggle('dark', t.isDark);
});

// React to changes
lifevis.onThemeChange(function(isDark) {
    document.documentElement.classList.toggle('dark', isDark);
});

11. Web Viewer Support

Add-ons can optionally support rendering in the web viewer (for shared links). When your add-on runs in the web viewer, it loads inside an iframe and receives data through a different bridge: window.parent provides the API instead of window.webkit.

To detect the web viewer context:

const isWebViewer = (window.parent !== window);

In the web viewer, window.lifevis is provided by the parent frame with compatible methods. Native actions (viewPost, editPost, editTag) and settings are not available — hide those controls when running in the web viewer.

To enable web viewer support, your add-on's catalog entry needs:

{
    "webSupported": true,
    "webURL": "https://relay.mylifevis.com/addons/com.yourname.myplugin/1.0.0/MyPlugin.html"
}

The web viewer provides search filtering. To support it:

// In the web viewer, listen for search changes
if (isWebViewer && lifevis.onSearchChange) {
    lifevis.onSearchChange(function(query) {
        filterAndRender(query);
    });
}

12. Testing Your Add-on

You don't need the LifeVis app or a server to develop and test add-ons. We provide a test harness that mocks the entire window.lifevis bridge API with realistic sample data. Your add-on runs in a browser exactly as it would inside the app.

Getting Started

Open the test harness directly in your browser: Launch Test Harness

Two ways to load your add-on:

No server, no install, no setup. The harness injects a mock window.lifevis bridge with sample data including:

Harness Features

Testing Checklist

  1. Add-on loads without errors (check the browser console)
  2. Light and dark themes render correctly
  3. Empty state displays properly (clear sample data)
  4. Settings drawer opens and saves work
  5. Clickable items trigger the correct viewPost/editTag calls
  6. Search filtering works (if supported)
  7. Config persists across page reloads
Tip: The test harness simulates the web viewer context when the plugin runs inside an iframe, so window.parent !== window will be true. To test the native app context (where isWebViewer is false), open your add-on HTML directly in the browser — the bridge won't be available, but you can test layout and styling.

13. Packaging & Submission

A add-on bundle is a .zip file containing:

MyPlugin.zip
  ├── MyPlugin.html     (required: entry point)
  ├── manifest.json     (written automatically on install, but included for reference)
  └── (optional additional assets)

Requirements:

To submit your add-on:

  1. Test thoroughly using the test harness
  2. Create a preview screenshot (1280x800 recommended)
  3. Use the contact form and select "Add-on Developer Submission" as the topic. Include:
    • A link to your add-on .zip bundle (hosted on your own server, GitHub, Dropbox, etc.)
    • Preview screenshot link
    • Brief description for the store listing
    • Whether you want it listed as free or paid (see Monetization)

We'll review your submission for security compliance (no network requests, no path traversal, CSP present) and functionality, then add it to the catalog.

14. Monetization

Add-ons can be listed as free or paid in the LifeVis add-on store.

Free Add-ons

Free add-ons are available to all LifeVis users at no cost. This is the fastest way to get your add-on listed and build a user base.

Paid Add-ons

Paid add-ons use Apple In-App Purchase, processed through the LifeVis app. When submitting, propose one of the available price tiers:

Price TierYou Receive (70%)LifeVis (30%)
$0.99~$0.49~$0.21
$1.99~$0.97~$0.42
$2.99~$1.46~$0.63
$4.99~$2.44~$1.05
$9.99~$4.89~$2.10

Amounts shown are after Apple's 30% platform fee (15% for qualifying small businesses). Revenue share is 70% developer / 30% LifeVis on net revenue.

How It Works

  1. Submit your add-on with your preferred price tier
  2. We create the In-App Purchase in App Store Connect and list your add-on
  3. Users purchase through the app — Apple handles payment processing
  4. You receive quarterly payouts via PayPal or bank transfer
You'll need to agree to the LifeVis Developer Revenue Share Agreement before your paid add-on can be listed. We'll send this when you submit a paid add-on.

Getting Started

Have an add-on idea? Questions about the API? Use the contact form — we're happy to help.