> Public copy of the ViewStream developer guide (also for AI agents). All guides: https://www.viewstream.co.il/developers/guide/index.md · API reference: https://api.viewstream.co.il/docs/

# Lovable and other browser-only apps

[Lovable](https://lovable.dev) (and Bolt, v0 in client mode, plain Vite + React) builds apps that run entirely in the
browser. Anything in that code is public, so **never put a ViewStream API key in it**. A browser app shows ViewStream
video with two public pieces instead:

| What | How | Key? |
|---|---|---|
| The list of videos, shows, catch-up | the [Sites Delivery API](sites-delivery-api.md) (`https://api.viewstream.co.il/s/v1/{site}/…`), public and cached | no |
| Playback | the hosted player in an `<iframe>` (`https://player.viewstream.co.il/e/{tenant}/vod/{id}`) | no |

Need uploads or private data? Keep those on a server you control (a Supabase Edge Function in a Lovable project, or a
Next.js app — see [nextjs.md](nextjs.md)); the key lives only there.

Working example (the same code, Vite + React): https://nextjs.viewstream.co.il/lovable/ — source
`integrations/lovable/example` in the ViewStream repository.

## 1. Allow the app in Studio

Studio → **Sites** → your site → **Headless**: turn on **Allow headless use** and add the app's address, for example
`https://my-app.lovable.app` (and your custom domain, and `http://localhost:5173` while developing). Without it the
browser blocks the requests (CORS).

Each request names the app's host in `?o=` — `o=my-app.lovable.app`, not the full URL — so the cached answer allows
exactly that origin.

If your account uses playback protection (Studio → Delivery → Protection) with an embed allow-list, add the app's
domain there too, or the player will not load inside it.

## 2. Paste this prompt into Lovable

Replace `<site>` (Studio → Sites, the site's slug) and `<tenant>` (your account's player name — `GET …/s/v1/<site>/config`
→ `player.tenant`).

```text
Build a Hebrew (RTL) video page with ViewStream. Do not use any API key.

Data (public, cached): let o = "o=" + window.location.host
1. GET https://api.viewstream.co.il/s/v1/<site>/route?path=/&<o>  → { page_id }
2. GET https://api.viewstream.co.il/s/v1/<site>/pages/<page_id>?sections=6&<o>
   → sections[].items[]; use the items with type "asset": id, title (a string or {he, en}), description,
     images.poster (16:9 thumbnail URL).
Show the first video in a player at the top and the others as a grid of thumbnails; clicking one plays it.

Player: <iframe src="https://player.viewstream.co.il/e/<tenant>/vod/<id>?lang=he"
  allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="width:100%;aspect-ratio:16/9;border:0">
```

## 3. The code it needs

```jsx
const API = 'https://api.viewstream.co.il/s/v1/<site>';
const o = `o=${location.host}`;               // headless: this app's host, allowed in Studio

const route = await fetch(`${API}/route?path=/&${o}`).then((r) => r.json());
const page = await fetch(`${API}/pages/${route.page_id}?sections=6&${o}`).then((r) => r.json());
const videos = page.sections.flatMap((s) => s.items || []).filter((i) => i.type === 'asset');

<iframe src={`https://player.viewstream.co.il/e/<tenant>/vod/${video.id}?lang=he`}
  allow="autoplay; fullscreen; picture-in-picture" allowFullScreen />
```

More routes (one video, a show and its episodes, live channels with now/next, the guide, search) are in
[sites-delivery-api.md](sites-delivery-api.md); a live channel plays with `…/e/<tenant>/live/<channel>`.

## Troubleshooting

| Symptom | Cause |
|---|---|
| `blocked by CORS policy` in the browser console | the app's address is not on the site's Headless list, or `o=` is missing or is a full URL instead of the host |
| The player area stays grey | your protection policy's embed allow-list does not include the app's domain |
| Empty list | the site has no published page at `/`, or the page has no video rails — add one in Studio → Sites |
