Example Application

Streams

An infinite scroll product discovery feed powered by image search. Upload an inspiration image and explore an endless stream of matching products.

Streams preview screenshot
Streams preview from the repository README.

Overview

Streams showcases the core TasteBrain workflow:

  1. Upload: User uploads an inspiration image
  2. Discover: App generates an infinite scrolling feed of matching products
  3. Navigate: User explores personalized product recommendations

Features

  • 📸 Image upload with drag-and-drop support
  • ∞ Infinite scroll product feed
  • 👤 Session-based user tracking (no auth required)
  • 🎨 Tailwind CSS 4 styling
  • ⚡ Server-side rendering for performance
  • 🖼️ Cloudinary CDN integration

Tech Stack

LayerTechnology
FrameworkSvelteKit 2 with Svelte 5
StylingTailwind CSS 4
Product APIBestomer Prism (personalized search)
Image HostingCloudinary
Sessionsvelte-kit-sessions

Prerequisites

  • Node.js 18+ with pnpm
  • Prism API credentials
  • Cloudinary account (free tier works)

Setup

1. Install Dependencies

cd examples/streams
pnpm install

2. Configure Environment

cp .env.example .env

Required variables:

# Session secret (generate with: openssl rand -hex 32)
SESSION_SECRET=your_session_secret_here

# Prism API configuration
PRISM_URL=https://api-prism.bestomer.io
PRISM_SECRET=your_prism_api_secret_here

# Cloudinary configuration
PUBLIC_CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name

3. Run Development Server

pnpm run dev

Visit http://localhost:3500

Key Implementation Details

1. Image Upload Flow

async function handleFileUpload(file: File) {
  // Upload to Cloudinary
  const formData = new FormData();
  formData.append('file', file);
  formData.append('upload_preset', 'your_preset');

  const response = await fetch(
    `https://api.cloudinary.com/v1_1/${CLOUD_NAME}/image/upload`,
    { method: 'POST', body: formData }
  );

  const data = await response.json();
  return data.secure_url;  // CDN URL
}

2. Prism API Integration

export const POST: RequestHandler = async ({ request, locals }) => {
  const { imageUrl } = await request.json();
  const userId = locals.session.userId;  // From session

  const response = await fetch('https://api-prism.bestomer.io/search/unified', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${PRISM_SECRET}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      queries: [{ image_url: imageUrl }],
      n_products: 48,
      n_pool: 500,
      noise: 0.0,
      user_id: userId  // Enable personalization
    })
  });

  const data = await response.json();
  return json({ products: data.results });
};

3. Infinite Scroll

let products = [];
let page = 0;
let loading = false;

async function loadMore() {
  if (loading) return;
  loading = true;

  const response = await fetch(`/api/feeds?page=${page}`);
  const newProducts = await response.json();

  products = [...products, ...newProducts];
  page++;
  loading = false;
}

// Intersection Observer for auto-load
onMount(() => {
  const observer = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting) loadMore();
  }, { threshold: 0.5 });

  observer.observe(document.querySelector('#scroll-sentinel'));
});

Project Structure

src/
├── hooks.server.ts              # Server hooks (session, env validation)
├── lib/
│   ├── components/
│   │   ├── MediaUploader.svelte     # Image upload component
│   │   ├── ProductFeed.svelte       # Infinite scroll feed
│   │   ├── ProductCard.svelte       # Individual product display
│   │   └── ProductSkeleton.svelte   # Loading skeleton
│   ├── types/
│   │   └── api.ts                   # Prism API type definitions
│   └── utils/
│       ├── api-client.ts            # Prism API client wrapper
│       └── cloudinary.ts            # Cloudinary upload helper
└── routes/
    ├── +page.svelte                 # Main page
    └── api/
        ├── feeds/+server.ts         # Feed generation endpoint
        └── seeds/+server.ts         # Seed management endpoint

Deployment

This app uses @sveltejs/adapter-node for deployment to Node.js environments.

Build for Production

pnpm run build
node build/index.js

Deploy to Cloud Platforms

Vercel (Recommended):

pnpm install -g vercel
vercel

Customization Guide

Change Product Count

body: JSON.stringify({
  queries: [...],
  n_products: 48,  // Change this (typical: 24-48)
  n_pool: 500,     // Candidate pool (typical: 300-1000)
  noise: 0.0
})

Add Brand Filtering

body: JSON.stringify({
  queries: [...],
  n_products: 30,
  n_pool: 300,
  noise: 0.0,
  domains: ['everlane.com', 'patagonia.com']  // Add this
})

Related Examples