How to Build a Privacy‑First Dining Recommender Component Using Local Profiles and LLMs
tutorialprivacyrecommendation

How to Build a Privacy‑First Dining Recommender Component Using Local Profiles and LLMs

jjavascripts
2026-02-12
10 min read
Advertisement

Build a privacy-first dining recommender: local profiles, federated similarity, and opt-in LLMs with code for React/Vue/vanilla/Web Components.

Stop leaking preferences. Ship a dining recommender that protects user data.

Decision fatigue is real: groups waste time arguing, apps ask for ever-more data, and teams ship recommendation widgets that become legal and maintenance nightmares. In 2026, you can build a fast, accurate dining recommender that keeps profiles local, uses federated similarity across peers, and optionally enriches suggestions with an LLM only when users opt in.

What you’ll learn

Why this matters in 2026

Since late 2024 and into 2026, the industry shifted: major platforms exposed on-device model runtimes and browsers accelerated WebGPU support. Apple’s move to pair system assistants with high-quality LLMs and the broader trend toward on-device inference changed expectations. Users now expect personalization without wholesale data exfiltration.

"Micro apps and personal apps exploded in 2024–2025. Developers and non-developers want local-first experiences that don't sell their data." — observed trend from 2024–2026 product launches and community work.

That makes a privacy-first dining recommender timely: small, fast, offline-capable, and respectful of user consent.

Architecture overview (privacy-first)

At a high level, the component has three layers:

  1. Local preference store — keep user tastes on-device in IndexedDB with encryption using Web Crypto.
  2. Federated similarity — compute vectors locally and exchange privacy-preserving sketches for group suggestions (no raw preferences leave devices).
  3. Optional LLM enrichment — run an on-device LLM or call a remote LLM only with explicit consent and tight telemetry controls. See best practices for LLM compliance.

Data flow (brief)

  • User records local interactions (likes, tags, visited) → encoded to a profile vector.
  • For group suggestion, devices exchange differentially private sketches or aggregated vectors via server or WebRTC.
  • Server returns candidate restaurants (IDs only) or aggregated rankings; final ranking happens locally.
  • If the user opts in, an LLM will re-rank or annotate suggestions; telemetry is minimal and opt-in.

Step 1 — Local preference store

Store user tastes locally using IndexedDB and protect them with symmetric encryption keys stored in the browser's credential store (Web Crypto). This prevents accidental leaks and makes export/import straightforward.

Schema

  • profiles: { id: 'local', version, tags: {cuisine: {italian: 3}, price: {"$": 1}}, history: [{restaurantId, timestamp, feedback}] }
  • vectors: {embeddingVersion, vector: Float32Array}
  • consent: {llm: boolean, telemetry: boolean}

Minimal IndexedDB helper (vanilla)

// open DB and put/get simple profile
const DB_NAME = 'dining-local';
function openDB(){
  return new Promise((resolve, reject)=>{
    const r = indexedDB.open(DB_NAME, 1);
    r.onupgradeneeded = e => {
      const db = e.target.result;
      db.createObjectStore('profiles', {keyPath:'id'});
      db.createObjectStore('vectors', {keyPath:'id'});
      db.createObjectStore('consent', {keyPath:'id'});
    };
    r.onsuccess = () => resolve(r.result);
    r.onerror = () => reject(r.error);
  });
}

async function put(store, obj){
  const db = await openDB();
  return new Promise((res, rej)=>{
    const tx = db.transaction(store, 'readwrite');
    tx.objectStore(store).put(obj);
    tx.oncomplete = () => res(true);
    tx.onerror = () => rej(tx.error);
  });
}

Encrypt any sensitive text before storage using Web Crypto; keep the key in a Credential or just derivable from user passphrase. For examples of micro-app patterns that use local encryption and IndexedDB, see our case studies.

Embedding generation (options)

  • On-device embedding: use lightweight transformers compiled to WASM/WebGPU (llama.cpp derivatives or WebLLM runtimes are common in 2026). If you need hardware-optimized runtimes, review edge bundle recommendations for small teams.
  • Server embedding: call an embedding API but only with explicit opt-in; return embeddings and store them locally, never log raw profile text.

Step 2 — Federated similarity (privacy-preserving)

Group recommendations must combine preferences without exposing raw profiles. There are multiple trade-offs: accuracy vs. privacy vs. bandwidth. We’ll present two pragmatic patterns you can implement today.

Devices compute local vectors and send encrypted, differentially noisy vectors to the server. The server performs secure aggregation (sum of vectors) and returns a combined vector (or candidate ranking). The server never sees individual vectors in plain text.

  1. Compute local vector v_i (normalized).
  2. Add small DP noise: v_i' = v_i + noise(σ).
  3. Encrypt v_i' with ephemeral key and send to aggregator endpoint.
  4. Server performs homomorphic-friendly aggregation or uses secure multi-party aggregation to derive Σ v_i'.
  5. Server returns aggregated vector (or top-K restaurant IDs) to participants.

This keeps individual vectors private and gives reasonably accurate group profiles. Use epsilon/δ settings suitable for your threat model. For infrastructure and deployment notes, consider resilient cloud-native architectures that support secure aggregation and short-lived tokens.

Pattern B — Peer-to-peer sketches via WebRTC

When you don’t want any central aggregator, devices can exchange compact sketches using WebRTC. Each device computes top-N hashed feature buckets or a random-projection sketch, applies differential privacy, and shares it with peers. Final recombination happens locally.

// toy sketch: 128-d random projection + clip + noise
function randomProjectionSketch(vec, proj){
  // proj: Float32Array(128 * vecLen)
  const sketch = new Float32Array(128);
  for(let i=0;i<128;i++){
    let sum = 0;
    for(let j=0;j<vec.length;j++) sum += vec[j] * proj[i*vec.length + j];
    sketch[i] = Math.tanh(sum); // clip
  }
  // add small gaussian noise for DP
  for(let i=0;i<sketch.length;i++) sketch[i] += gaussian(0, 0.02);
  return sketch;
}

Sketch exchange costs are low and you never send raw categories or history.

Step 3 — Candidate retrieval and local ranking

Retrieval strategies:

  • Local-only: keep a compact catalog (top 2–3K local restaurants) in IndexedDB. Perform ANN search locally via HNSW or Faiss ported to WASM.
  • Server-assisted: server returns candidates (IDs) based on aggregated vector; the client performs final scoring locally with the user's precise vector.

Key principle: the server should not be able to reconstruct the user's profile from what you send. Only exchange sketches or aggregated vectors.

Local cosine similarity example

function cosine(a, b){
  let da=0, db=0, dot=0;
  for(let i=0;i<a.length;i++){ dot += a[i]*b[i]; da += a[i]*a[i]; db += b[i]*b[i]; }
  return dot / (Math.sqrt(da)*Math.sqrt(db) + 1e-8);
}

// rank candidates
const scored = candidates.map(c=>({id:c.id, score:cosine(localVec, c.vec)})).sort((x,y)=>y.score-x.score);

LLMs provide helpful annotations (e.g., “Great for quiet late dinners”, or “Best for vegan options”). But in a privacy-first product, LLM access must be:

  • Opt-in — users toggle LLM features explicitly.
  • Minimal — only send candidate IDs, small descriptors, or on-device embeddings; avoid sending the user's entire profile.
  • Transparent telemetry — if you collect metrics, list them and provide a way to opt out and delete logs. See operational guidance in LLM compliance notes.

Run LLM on-device vs. remote

  • On-device LLM (preferred for privacy): use WebLLM runtimes or distilled models with WebGPU. The text never leaves the device; these runtimes are increasingly available in small edge bundles.
  • Remote LLM (opt-in): send candidate IDs or short prompts, not the full profile. Gather acceptance timestamps and model version if you log telemetry.

Example prompt (remote) minimizing data exposure

Prompt:
"Annotate these restaurant summaries for user-facing reasons to choose each. User preferences: vegetarian:true, prefers_ambience:"cozy". Restaurants: [ {id:123, short_desc:"Italian, pasta, wine bar"}, ... ]" 

Send only a small, sanitized prompt. If the user consents to telemetry, tag the record with a random request ID, not a user ID.

Integration examples

Below is a tiny shared API for the component that we’ll use across framework examples.

// public API (core library)
export async function initRecommender({catalog, userId}){ /* returns handle */ }
export async function getLocalProfile(handle){ /* IndexedDB read */ }
export async function updateFeedback(handle, event){ /* like/visit/tag */ }
export async function suggestForGroup(handle, peers, opts){ /* returns ranked IDs */ }
export async function enableLLM(handle, mode){ /* 'on-device' | 'remote' */ }

React (hooks) example

import React, {useEffect, useState} from 'react';
import {initRecommender, suggestForGroup} from 'dining-privacy-core';

export default function Where2EatWidget({catalog, peers}){
  const [r, setR] = useState(null);
  useEffect(()=>{ (async()=>{const h = await initRecommender({catalog}); setR(h);})(); },[]);

  async function suggest(){
    const ranking = await suggestForGroup(r, peers, {k:10});
    setSuggestions(ranking);
  }

  return (
{/* render suggestions */}
); }

Vue 3 (Composition API) example

import {ref, onMounted} from 'vue';
import {initRecommender, suggestForGroup} from 'dining-privacy-core';

export default {
  setup(props){
    const h = ref(null);
    const suggestions = ref([]);
    onMounted(async ()=>{ h.value = await initRecommender({catalog:props.catalog}); });
    async function suggest(){ suggestions.value = await suggestForGroup(h.value, props.peers, {k:10}); }
    return {suggest, suggestions};
  }
}

Vanilla JS usage

const handle = await initRecommender({catalog});
document.querySelector('#go').addEventListener('click', async ()=>{
  const ranked = await suggestForGroup(handle, peers, {k:5});
  render(ranked);
});

Web Component

class DiningRecommender extends HTMLElement{
  async connectedCallback(){
    this.handle = await initRecommender({catalog: this.catalog});
    this.innerHTML = '<button id=go>Suggest</button>';
    this.querySelector('#go').addEventListener('click', async ()=>{
      const r = await suggestForGroup(this.handle, this.peers, {k:5});
      this.render(r);
    });
  }
}
customElements.define('dining-recommender', DiningRecommender);

Measuring performance and accuracy (benchmarks)

Benchmarks depend on device, but here’s a practical testing plan you should run before shipping:

  1. Local embedding time (ms) for 1 input phrase on major device classes (mobile low/mid/high, laptop).
  2. Local ANN query latency for catalog sizes: 500, 2k, 10k.
  3. End-to-end group suggestion latency (including federated sketch exchange) for groups of 2, 5, 10 on real networks.

Example targets (achievable in 2026 with WASM/WebGPU):

  • Embedding (distilled model) per phrase: 15–120 ms depending on device.
  • ANN query (2k items): sub-50 ms on modern phones, sub-10 ms on desktops.
  • Federated aggregation (server mediated): round-trip 150–400 ms; WebRTC peer exchange scales with group size and network topology.
  • Accessibility: ensure the UI is keyboard-navigable, labels are present, and the LLM annotations are readable with clear semantics.
  • Security: use HTTPS, CSP, and HSTS; rotate ephemeral keys; verify WebRTC identity using short-lived tokens.
  • Legal/Privacy: document data flows, publish your DP parameters, and provide an easy data-deletion flow. For enterprise LLM and compliance considerations, see running LLMs on compliant infrastructure.

Real-world pattern: the Where2Eat micro app (case study)

Recall the micro app trend: people build tiny apps for social groups. A privacy-first Where2Eat stores users' likes locally, exchanges only sketches during a dinner decision, and only runs an LLM to generate a shared one-sentence summary when everyone opts in. The result: quick decisions, no long-term data collection, and minimal moderation needs.

Advanced strategies and future predictions (2026+)

  • Hybrid embeddings: combine on-device lightweight embeddings for privacy with server-side larger embeddings for cold-starts, but only when users opt in.
  • Secure enclaves in browsers: expect browser vendors to offer sealed computation enclaves (2026 previews) enabling stronger secure aggregation without heavy crypto plumbing.
  • Composable privacy policies: provide machine-readable privacy manifests so clients can automatically verify what data an LLM call will send.
  • Model provenance: by 2026 it's increasingly common to attach model fingerprints to responses — expose those so users and admins can audit behavior.

Checklist before production

  • Local store is encrypted; IndexedDB schema versioned.
  • Default behavior: no remote calls, no LLM requests, no telemetry.
  • Consent screen with clear toggles and explanations (include an explicit opt-in for telemetry and LLMs; see product support playbooks like Tiny Teams, Big Impact).
  • DP parameters and aggregation protocol documented and auditable. Keep IaC and CI test templates handy — see IaC templates for automated verification.
  • Continuous benchmarks integrated into CI for model versions and catalog size changes.

Final code snippet: pulling everything together (simplified)

async function groupSuggestFlow(catalog, peers){
  const handle = await initRecommender({catalog});
  // compute local embedding
  const profile = await getLocalProfile(handle);
  const localVec = await embedProfile(profile); // on-device or opt-in

  // create sketch and send to aggregator
  const sketch = createSketch(localVec);
  const aggregated = await fetch('/aggregate', {method:'POST', body: JSON.stringify({sketch})});
  const candidates = await aggregated.json();

  // final rank locally
  const ranked = rankLocally(localVec, candidates);

  // optional LLM annotation (explicit opt-in)
  if(profile.consent.llm){
    const annotations = await annotateWithLLM(ranked.slice(0,5), {mode:profile.llmMode});
    return ranked.map((r,i)=>({...r, note: annotations[i]}));
  }
  return ranked;
}

Takeaways

  • Local profiles plus lightweight on-device inference dramatically reduce privacy risk and integration friction.
  • Federated similarity (secure aggregation or P2P sketches) lets you provide accurate group suggestions without sending raw preferences to the server.
  • LLM enrichment is powerful but must be opt-in, minimal, and auditable — prefer on-device when possible. For production readiness and compliance, review LLM operational guidance.
  • In 2026, the tooling to run this architecture at scale is available: WebGPU, WASM LLM runtimes, and improved secure aggregation patterns.

Next steps — start shipping

Want a production-ready starter kit? Clone the privacy-first starter that includes IndexedDB encryption, a federated sketch endpoint, a WASM embedding runner, and React/Vue/Web Component examples. It’s designed for integration with your existing catalog and has opt-in telemetry baked in. If you need commercial packages and maintenance guarantees, evaluate curated tools & marketplaces that follow these patterns.

Call to action: Try the starter kit, run the benchmark suite on your devices, and integrate the component into a micro-app or team tool. If you need a vetted commercial component with maintenance guarantees, licensing options, and enterprise telemetry compliance, visit javascripts.shop to evaluate curated packages that follow the privacy-first patterns described here.

Advertisement

Related Topics

#tutorial#privacy#recommendation
j

javascripts

Contributor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

Advertisement
2026-02-12T22:04:48.475Z