Developers
Public data
Everything Bluvy Tube writes is a public AT Protocol record, stored in the user’s repository. This page describes those records, explains how to read them, and lists the few HTTP endpoints we expose on top.
The principle
Bluvy Tube doesn’t keep video data to itself. It reads and writes it in each user’s PDS, with their permission (OAuth). For a video, you’ll find:
- a standard
app.bsky.feed.postpost, which carries the video (app.bsky.embed.video) and stays visible in Bluesky and every compatible app; - an optional
app.bluvy.tube.video.metadatarecord, holding what Bluesky doesn’t support: title, long description, category, chapters, captions, custom thumbnail; - our index (views, trending, search), derived from that data and not exposed as an API.
app.bluvy.tube.* lexicons
The schemas are published following the lexicon resolution convention. The _lexicon.tube.bluvy.app DNS record points to Bluvy Tube’s account, which hosts each schema as a com.atproto.lexicon.schema record, with the NSID as rkey.
$ dig +short TXT _lexicon.tube.bluvy.app
"did=did:plc:dgqudubx2plnmynqu5wtsoan"
$ curl "https://eurosky.social/xrpc/com.atproto.repo.getRecord?repo=did:plc:dgqudubx2plnmynqu5wtsoan&collection=com.atproto.lexicon.schema&rkey=app.bluvy.tube.video.metadata"
That account’s PDS (currently eurosky.social) may change: resolve it from the DID document instead of hard-coding it. The tables below are generated from the app’s schema files. An asterisk marks a required field.
app.bluvy.tube.authFull
permission-set
The permissions Bluvy Tube requests at OAuth sign-in.
Collections covered: app.bluvy.tube.favorite, app.bluvy.tube.playlist, app.bluvy.tube.playlist.item, app.bluvy.tube.video.metadata, app.bluvy.tube.preferences
Show the full JSON schema
{
"lexicon": 1,
"id": "app.bluvy.tube.authFull",
"defs": {
"main": {
"type": "permission-set",
"title": "Bluvy Tube",
"title:lang": {
"fr": "Bluvy Tube"
},
"detail": "Bluvy Tube, the video platform for Bluesky, saves your favorites, playlists, app preferences and the details of the videos you publish (title, description, category, chapters, captions) as app.bluvy.tube.* records in your account.",
"detail:lang": {
"fr": "Bluvy Tube, la plateforme vidéo pour Bluesky, enregistre vos favoris, vos playlists, vos préférences d'application et les informations des vidéos que vous publiez (titre, description, catégorie, chapitres, sous-titres) sous forme d'enregistrements app.bluvy.tube.* dans votre compte."
},
"permissions": [
{
"type": "permission",
"resource": "repo",
"collection": [
"app.bluvy.tube.favorite",
"app.bluvy.tube.playlist",
"app.bluvy.tube.playlist.item",
"app.bluvy.tube.video.metadata"
],
"action": ["create", "update", "delete"]
},
{
"type": "permission",
"resource": "repo",
"collection": ["app.bluvy.tube.preferences"],
"action": ["create", "update"]
}
]
}
}
}
app.bluvy.tube.favorite
record
rkey: tid A favorite (the “Saved” section): a strong reference to a video post. It is not a like.
| Field | Type | Description |
|---|---|---|
subject * |
ref → com.atproto.repo.strongRef |
The video post (an app.bsky.feed.post carrying an app.bsky.embed.video) being favorited. |
createdAt * |
string (datetime) |
Client-declared timestamp when this favorite was created. |
Show the full JSON schema
{
"lexicon": 1,
"id": "app.bluvy.tube.favorite",
"defs": {
"main": {
"type": "record",
"description": "A Bluvy Tube-specific favorite of a video post, written to the favoriting user's own repo. Distinct from any AT Protocol-native bookmark — this is a Bluvy-only concept, source of truth for Bluvy Tube's own 'favorites' feature (not a like or repost). See App\\Controller\\Account\\FavoriteController in the Bluvy Tube source for the writer.",
"key": "tid",
"record": {
"type": "object",
"required": ["subject", "createdAt"],
"properties": {
"subject": {
"type": "ref",
"ref": "com.atproto.repo.strongRef",
"description": "The video post (an app.bsky.feed.post carrying an app.bsky.embed.video) being favorited."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this favorite was created."
}
}
}
}
}
}
app.bluvy.tube.playlist
record
rkey: tid A user’s playlist. Each of its videos is a separate app.bluvy.tube.playlist.item record.
| Field | Type | Description |
|---|---|---|
name * |
string |
The playlist's display name. |
description |
string |
Optional free-text description of the playlist. |
createdAt * |
string (datetime) |
Client-declared timestamp when this playlist was created. |
Show the full JSON schema
{
"lexicon": 1,
"id": "app.bluvy.tube.playlist",
"defs": {
"main": {
"type": "record",
"description": "A Bluvy Tube playlist — a named, ordered-by-creation collection of videos, written to the owning user's own repo. Membership is tracked separately by app.bluvy.tube.playlist.item records referencing this record via strongRef, the same 'list + separate items' shape as app.bsky.graph.list/listitem. See App\\Controller\\Account\\PlaylistController in the Bluvy Tube source for the writer.",
"key": "tid",
"record": {
"type": "object",
"required": ["name", "createdAt"],
"properties": {
"name": {
"type": "string",
"maxGraphemes": 100,
"maxLength": 300,
"description": "The playlist's display name."
},
"description": {
"type": "string",
"maxGraphemes": 500,
"maxLength": 3000,
"description": "Optional free-text description of the playlist."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this playlist was created."
}
}
}
}
}
}
app.bluvy.tube.playlist.item
record
rkey: tid A video in a playlist: a strong reference to the playlist and to the video post.
| Field | Type | Description |
|---|---|---|
playlist * |
ref → com.atproto.repo.strongRef |
The app.bluvy.tube.playlist record this item belongs to. |
subject * |
ref → com.atproto.repo.strongRef |
The video post (an app.bsky.feed.post carrying an app.bsky.embed.video) added to the playlist. |
createdAt * |
string (datetime) |
Client-declared timestamp when the video was added — also this item's own place in the playlist's display order (oldest first). |
Show the full JSON schema
{
"lexicon": 1,
"id": "app.bluvy.tube.playlist.item",
"defs": {
"main": {
"type": "record",
"description": "One video's membership in a app.bluvy.tube.playlist, written to the playlist owner's own repo as a separate record (not embedded in the playlist record itself) — same shape as app.bsky.graph.listitem referencing an app.bsky.graph.list. See App\\Controller\\Account\\PlaylistController in the Bluvy Tube source for the writer.",
"key": "tid",
"record": {
"type": "object",
"required": ["playlist", "subject", "createdAt"],
"properties": {
"playlist": {
"type": "ref",
"ref": "com.atproto.repo.strongRef",
"description": "The app.bluvy.tube.playlist record this item belongs to."
},
"subject": {
"type": "ref",
"ref": "com.atproto.repo.strongRef",
"description": "The video post (an app.bsky.feed.post carrying an app.bsky.embed.video) added to the playlist."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when the video was added — also this item's own place in the playlist's display order (oldest first)."
}
}
}
}
}
}
app.bluvy.tube.preferences
record
rkey: literal:self The user’s appearance preferences (theme, palette…). One record per account.
| Field | Type | Description |
|---|---|---|
mode * |
string |
Colour-mode axis. 'system' follows the OS prefers-color-scheme. |
darkStyle |
string |
Dark-mode depth. Absent means 'dim' (the default #070D1C ramp); 'black' is the opt-in near-black OLED ramp (#05070D, never pure #000000). |
palette |
string |
Ecosystem palette. Absent means the Bluvy Tube default palette (logo-derived). The others are opt-in alternates. |
accent |
string |
Logo-derived accent override for primary CTAs. Absent means the Bluvy Blue signature (#185DFF). |
fontFamily |
string |
UI font family. Absent means the native system stack. |
fontSize |
string |
Root text size. Absent means 'default' (100%). |
linkAppView |
string |
Which AT Protocol AppView web frontend external post/profile links (e.g. 'view original post') open through. Absent means 'mu' (Mu.social, Bluvy Tube's default). |
feedMode |
string |
Primary feed navigation and video layout mode. 'youtube' is the standard grid of thumbnails; 'immersive' is the continuous full-screen vertical swipe feed. Absent means 'youtube'. |
breakReminderMinutes |
integer |
Wellbeing reminder: minutes of actual playback in vertical swipe feeds before Bluvy Tube interrupts with a take-a-break prompt. Absent means 30. There is intentionally no value that disables the reminder; unknown values are read back as 30. |
updatedAt * |
string (datetime) |
Client-declared timestamp of the last change. |
Show the full JSON schema
{
"lexicon": 1,
"id": "app.bluvy.tube.preferences",
"defs": {
"main": {
"type": "record",
"description": "Bluvy Tube-specific per-app appearance preferences for one account, written to that account's own repo at rkey `self`. Not required or validated by the PDS or any other AT Protocol app — Bluvy Tube reads this back itself at login to server-render the correct theme and remove the theme flash-of-unstyled-content. Each Bluvy app keeps its own independent record in its own namespace; this one is Bluvy Tube's. See App\\Service\\AppearanceService in the Bluvy Tube source for the writer.",
"key": "literal:self",
"record": {
"type": "object",
"required": ["mode", "updatedAt"],
"properties": {
"mode": {
"type": "string",
"enum": ["light", "dark", "system"],
"description": "Colour-mode axis. 'system' follows the OS prefers-color-scheme."
},
"darkStyle": {
"type": "string",
"enum": ["dim", "black"],
"description": "Dark-mode depth. Absent means 'dim' (the default #070D1C ramp); 'black' is the opt-in near-black OLED ramp (#05070D, never pure #000000)."
},
"palette": {
"type": "string",
"enum": ["bluvy", "bluesky", "blacksky", "mu"],
"description": "Ecosystem palette. Absent means the Bluvy Tube default palette (logo-derived). The others are opt-in alternates."
},
"accent": {
"type": "string",
"enum": ["bluvy-blue", "bluvy-cyan", "bluvy-deep"],
"description": "Logo-derived accent override for primary CTAs. Absent means the Bluvy Blue signature (#185DFF)."
},
"fontFamily": {
"type": "string",
"enum": ["system", "inter"],
"description": "UI font family. Absent means the native system stack."
},
"fontSize": {
"type": "string",
"enum": ["small", "default", "large"],
"description": "Root text size. Absent means 'default' (100%)."
},
"linkAppView": {
"type": "string",
"enum": ["bluesky", "mu", "blacksky"],
"description": "Which AT Protocol AppView web frontend external post/profile links (e.g. 'view original post') open through. Absent means 'mu' (Mu.social, Bluvy Tube's default)."
},
"feedMode": {
"type": "string",
"enum": ["youtube", "immersive"],
"description": "Primary feed navigation and video layout mode. 'youtube' is the standard grid of thumbnails; 'immersive' is the continuous full-screen vertical swipe feed. Absent means 'youtube'."
},
"breakReminderMinutes": {
"type": "integer",
"enum": [15, 30, 45, 60, 90, 120],
"description": "Wellbeing reminder: minutes of actual playback in vertical swipe feeds before Bluvy Tube interrupts with a take-a-break prompt. Absent means 30. There is intentionally no value that disables the reminder; unknown values are read back as 30."
},
"updatedAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp of the last change."
}
}
}
}
}
}
app.bluvy.tube.video.metadata
record
rkey: tid Bluvy metadata for a published video, stored next to the post that carries the video. postUri and postCid point to that post, and url is the stable address of the page on Bluvy Tube.
| Field | Type | Description |
|---|---|---|
video * |
blob |
The same video blob referenced by the companion app.bsky.feed.post's app.bsky.embed.video#video. |
createdAt * |
string (datetime) |
Client-declared timestamp when this record was created. |
title |
string |
Owner-editable title, shown on Bluvy Tube in place of the raw post text. |
description |
string |
|
language |
string |
BCP-47 language tag for the video's primary spoken/subtitle language. |
category |
string |
Primary category slug (e.g. 'gaming', 'tech', 'music', etc.). |
secondaryCategories |
array<string> |
Optional secondary category slugs. |
postUri |
string (at-uri) |
The at-uri of the companion app.bsky.feed.post record, set once that record has been created (this metadata record is written first, in a two-step publish). |
postCid |
string (cid) |
The CID of the companion app.bsky.feed.post record as of the last write of this record; together with postUri, a strong reference to it. Set together with postUri. |
url |
string (uri) |
The Bluvy Tube web page that renders this video (a stable https://{host}/at/{did}/app.bsky.feed.post/{rkey} address). Attests the page in the record-to-page direction; the page links back via an at:canonical meta tag. Set together with postUri. |
duration |
integer |
Duration of the video in seconds. |
thumbnail |
blob |
A thumbnail the owner chose for this video (at most 1280 px on each side). Bluvy Tube shows it instead of the frame generated by the video service; the companion post keeps the generated one. |
isAiGenerated |
boolean |
Whether the video content was generated by AI. |
chapters |
array<object> |
Owner-editable chapter markers shown on Bluvy Tube's scrubber. |
captions |
array<object> |
Owner-uploaded WebVTT subtitle/caption tracks, one per language. |
Show the full JSON schema
{
"lexicon": 1,
"id": "app.bluvy.tube.video.metadata",
"defs": {
"main": {
"type": "record",
"description": "Bluvy Tube-specific metadata for a published video, written to the creator's own repo alongside the app.bsky.feed.post record that actually carries the video (via app.bsky.embed.video). Not required or validated by the PDS or any other AT Protocol app — Bluvy Tube reads this back itself to render an owner-editable title/description/language overlay on top of the plain post text. Written by App\\Service\\VideoPublicationProcessor (native uploads) and rewritten by App\\Service\\PdsMetadataSyncService on every owner edit, in the Bluvy Tube source.",
"key": "tid",
"record": {
"type": "object",
"required": ["video", "createdAt"],
"properties": {
"video": {
"type": "blob",
"accept": ["video/*"],
"description": "The same video blob referenced by the companion app.bsky.feed.post's app.bsky.embed.video#video."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this record was created."
},
"title": {
"type": "string",
"maxGraphemes": 300,
"maxLength": 3000,
"description": "Owner-editable title, shown on Bluvy Tube in place of the raw post text."
},
"description": {
"type": "string",
"maxGraphemes": 3000,
"maxLength": 30000
},
"language": {
"type": "string",
"description": "BCP-47 language tag for the video's primary spoken/subtitle language."
},
"category": {
"type": "string",
"description": "Primary category slug (e.g. 'gaming', 'tech', 'music', etc.)."
},
"secondaryCategories": {
"type": "array",
"items": { "type": "string" },
"description": "Optional secondary category slugs."
},
"postUri": {
"type": "string",
"format": "at-uri",
"description": "The at-uri of the companion app.bsky.feed.post record, set once that record has been created (this metadata record is written first, in a two-step publish)."
},
"postCid": {
"type": "string",
"format": "cid",
"description": "The CID of the companion app.bsky.feed.post record as of the last write of this record; together with postUri, a strong reference to it. Set together with postUri."
},
"url": {
"type": "string",
"format": "uri",
"description": "The Bluvy Tube web page that renders this video (a stable https://{host}/at/{did}/app.bsky.feed.post/{rkey} address). Attests the page in the record-to-page direction; the page links back via an at:canonical meta tag. Set together with postUri."
},
"duration": {
"type": "integer",
"description": "Duration of the video in seconds."
},
"thumbnail": {
"type": "blob",
"accept": ["image/jpeg"],
"maxSize": 2000000,
"description": "A thumbnail the owner chose for this video (at most 1280 px on each side). Bluvy Tube shows it instead of the frame generated by the video service; the companion post keeps the generated one."
},
"isAiGenerated": {
"type": "boolean",
"description": "Whether the video content was generated by AI."
},
"chapters": {
"type": "array",
"maxLength": 50,
"description": "Owner-editable chapter markers shown on Bluvy Tube's scrubber.",
"items": {
"type": "object",
"required": ["title", "startTimeSeconds"],
"properties": {
"title": {
"type": "string",
"maxGraphemes": 100,
"maxLength": 500
},
"startTimeSeconds": {
"type": "integer",
"minimum": 0
}
}
}
},
"captions": {
"type": "array",
"maxLength": 20,
"description": "Owner-uploaded WebVTT subtitle/caption tracks, one per language.",
"items": {
"type": "object",
"required": ["lang", "vtt"],
"properties": {
"lang": {
"type": "string",
"description": "BCP-47 language tag for this track, e.g. 'fr' or 'en-US'."
},
"vtt": {
"type": "blob",
"accept": ["text/vtt"],
"description": "WebVTT subtitle/caption file for this language."
}
}
}
}
}
}
}
}
}
The old tube.bluvy.* NSIDs are no longer written. Bluvy Tube migrates existing records to app.bluvy.tube.*, then deletes the old ones. Don’t build on the old namespace.
Reading the data
From a user’s repository
Records are public. They can be read without authentication from the user’s PDS, with the standard XRPC methods:
# 1. Find the account's PDS in its DID document (did:plc → plc.directory, did:web → /.well-known/did.json)
curl https://plc.directory/did:plc:abc123
# → "service": [{ "id": "#atproto_pds", "serviceEndpoint": "https://pds.example.com" }]
# 2. List the Bluvy video metadata records of that account
curl "https://pds.example.com/xrpc/com.atproto.repo.listRecords?repo=did:plc:abc123&collection=app.bluvy.tube.video.metadata&limit=50"
# 3. Fetch one record (any collection: app.bluvy.tube.playlist, app.bluvy.tube.favorite…)
curl "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:abc123&collection=app.bluvy.tube.playlist&rkey=3kxyz"
In real time with Jetstream
To follow creations, updates and deletions across the whole network as they happen, subscribe to Jetstream and filter on our collections:
const url = new URL('wss://jetstream2.us-east.bsky.network/subscribe');
for (const nsid of ['app.bluvy.tube.video.metadata', 'app.bluvy.tube.playlist', 'app.bluvy.tube.playlist.item']) {
url.searchParams.append('wantedCollections', nsid);
}
const ws = new WebSocket(url);
ws.onmessage = (message) => {
const event = JSON.parse(message.data);
if (event.kind !== 'commit') return;
const { operation, collection, rkey, record } = event.commit;
console.log(operation, `at://${event.did}/${collection}/${rkey}`, record);
};
Linking a record to its page
The url field of app.bluvy.tube.video.metadata gives the page’s address. In the other direction, the page links back to its records through its at-tags. For any other AT URI, the /at resolver finds the matching page.
Media
Videos are Bluesky blobs. Bluesky’s video CDN serves them as HLS, from the author’s DID and the blob CID (embed.video.ref in the post):
https://video.bsky.app/watch/{did}/{cid}/playlist.m3u8 # HLS stream (adaptive)
https://video.bsky.app/watch/{did}/{cid}/thumbnail.jpg # default thumbnail
https://{pds}/xrpc/com.atproto.sync.getBlob?did={did}&cid={thumbnailCid} # custom thumbnail blob
Custom thumbnails are the thumbnail blob of the metadata record, fetched from the creator’s PDS. Respect creators’ choices: a video deleted from their repository should disappear from your app too.
Stable HTTP endpoints
These endpoints are public and need no authentication. They are the ones you can rely on. The language prefix is fr or en.
| Endpoint | Response | Description |
|---|---|---|
GET /oembed?url=… | JSON | Embed code and metadata for a video. Details |
GET /at?uri=at://…GET /at/{repo}[/{collection}/{rkey}] | 302 | Redirects an AT URI (post, metadata, playlist, profile, live) to its page. Details |
GET /embed/… | HTML | The embeddable player. Details |
GET /{locale}/video/{id}/captions/{lang}.vttGET /{locale}/video/{id}/chapters.vtt | text/vtt | A video’s captions and chapters, as WebVTT. A missing track returns a 404. |
GET /sitemap.xml | XML | Public videos and channels, in French and English (hreflang alternates). Regenerated every 30 minutes. |
GET /robots.txt | text | Crawling rules. Please follow them. |
GET /.well-known/atproto-did | text | Bluvy Tube’s DID, for verifying the tube.bluvy.app handle. |
GET /client-metadata.json | JSON | Bluvy Tube’s OAuth client metadata (its client_id). Also served at /oauth/client-metadata.json. |
Internal endpoints
These endpoints power the site’s interface. They are public but come with no stability guarantee: their format, parameters and limits may change at any time. The other /api/… routes are reserved for the Bluvy Tube extension and indexer.
| Endpoint | Description |
|---|---|
GET /api/video-stats?ids[]=… | Counters (likes, replies, reposts) for up to 25 videos, identified by their internal numeric id. Counters older than 5 minutes are refreshed from Bluesky. |
GET /{locale}/search/suggest?q=… | Search bar suggestions: queries, hashtags, videos, creators. |
Rate limits and caching
Limits apply per IP address, over a sliding window. Beyond them, the response is a 429.
| Endpoint | Limit | Cache |
|---|---|---|
/oembed | 120 requests / minute | public, max-age=3600 |
/{locale}/search/suggest | 120 requests / minute | — |
/embed/… | — | public, max-age=120, stale-while-revalidate=600 |
/sitemap.xml | — | Server-side, 30 min |
Moderation
On top of its own content moderation, Bluvy Tube applies labels from the following moderation services (atproto-accept-labelers header):
did:plc:ar7c4by46qjdydhdevvrndac— Bluesky’s official moderation
Bluvy Tube doesn’t publish a feed generator or a labeler yet. If you are building one for video, let’s talk.