Skip to main content
Each example includes the editor panel and OBS layer. Copy them as a starting point and adapt to your needs.

Text Overlay

A basic extension that lets the streamer type text and toggle its visibility on stream. What it does: Toggle + text field in settings, heading on the OBS overlay.

Editor — src/admin.tsx

import {
  Vision,
  CompactView,
  Text,
  Toggle,
  TextField,
  useExtensionStorage,
} from "@1upvision/sdk";

function Settings() {
  const [storage, setStorage] = useExtensionStorage();

  return (
    <CompactView title="Text Overlay">
      <Text content="Show custom text on your stream." variant="muted" />
      <Toggle
        label="Visible"
        checked={storage.visible ?? false}
        onChange={(checked) => setStorage({ ...storage, visible: checked })}
      />
      <TextField
        label="Display Text"
        placeholder="Enter text to show on stream..."
        value={storage.text ?? ""}
        onChange={(value) => setStorage({ ...storage, text: value })}
      />
    </CompactView>
  );
}

Vision.render(<Settings />, { target: "editor" });

Layer — src/layer.tsx

import { Vision, Text, useExtensionStorage } from "@1upvision/sdk";

function Overlay() {
  const [storage] = useExtensionStorage();

  if (!storage.visible) return null;

  return <Text content={storage.text ?? ""} variant="heading" />;
}

Vision.render(<Overlay />, { target: "layer" });

Score Counter

Let the streamer set and reset a score. The current score displays on the OBS overlay. What it does: Number input + reset button in settings, score display on stream.

Editor — src/admin.tsx

import {
  Vision,
  CompactView,
  Text,
  Button,
  NumberField,
  useExtensionStorage,
} from "@1upvision/sdk";

function Settings() {
  const [storage, setStorage] = useExtensionStorage();
  const score = storage.score ?? 0;

  return (
    <CompactView title="Score Counter">
      <Text content={`Current Score: ${score}`} variant="bold" />
      <NumberField
        label="Set Score"
        value={score}
        min={0}
        onChange={(value) => setStorage({ ...storage, score: value })}
      />
      <Button
        label="Reset to 0"
        variant="outline"
        onClick={() => setStorage({ ...storage, score: 0 })}
      />
    </CompactView>
  );
}

Vision.render(<Settings />, { target: "editor" });

Layer — src/layer.tsx

import { Vision, Text, useExtensionStorage } from "@1upvision/sdk";

function Overlay() {
  const [storage] = useExtensionStorage();

  return <Text content={`Score: ${storage.score ?? 0}`} variant="heading" />;
}

Vision.render(<Overlay />, { target: "layer" });

Stream Info Panel

A richer example with multiple field types — text inputs, a dropdown, a toggle, and an image. What it does: Full settings panel with title, description, theme picker, and optional avatar. The layer renders a styled info card.

Editor — src/admin.tsx

import {
  Vision,
  CompactView,
  List,
  Text,
  TextField,
  TextArea,
  Dropdown,
  Toggle,
  useExtensionStorage,
} from "@1upvision/sdk";

function Settings() {
  const [storage, setStorage] = useExtensionStorage();

  return (
    <CompactView title="Stream Info">
      <Text
        content="Configure what info appears on your overlay."
        variant="muted"
      />

      <List gap={16}>
        <TextField
          label="Stream Title"
          placeholder="Today's stream..."
          value={storage.title ?? ""}
          onChange={(value) => setStorage({ ...storage, title: value })}
        />

        <TextArea
          label="Description"
          placeholder="What are we doing today?"
          value={storage.description ?? ""}
          rows={3}
          onChange={(value) => setStorage({ ...storage, description: value })}
        />

        <Dropdown
          label="Theme"
          value={storage.theme ?? "dark"}
          options={[
            { label: "Dark", value: "dark" },
            { label: "Light", value: "light" },
            { label: "Transparent", value: "transparent" },
          ]}
          onChange={(value) => setStorage({ ...storage, theme: value })}
        />

        <Toggle
          label="Show avatar"
          checked={storage.showAvatar ?? true}
          onChange={(checked) =>
            setStorage({ ...storage, showAvatar: checked })
          }
        />

        <TextField
          label="Avatar URL"
          placeholder="https://..."
          value={storage.avatarUrl ?? ""}
          disabled={!(storage.showAvatar ?? true)}
          onChange={(value) => setStorage({ ...storage, avatarUrl: value })}
        />
      </List>
    </CompactView>
  );
}

Vision.render(<Settings />, { target: "editor" });

Layer — src/layer.tsx

import {
  Vision,
  CompactView,
  List,
  Text,
  Image,
  useExtensionStorage,
} from "@1upvision/sdk";

function Overlay() {
  const [storage] = useExtensionStorage();

  return (
    <CompactView>
      <List gap={8}>
        {storage.showAvatar && storage.avatarUrl ? (
          <Image src={storage.avatarUrl} alt="Avatar" width={64} height={64} />
        ) : null}
        <Text content={storage.title ?? ""} variant="heading" />
        {storage.description ? (
          <Text content={storage.description} variant="muted" />
        ) : null}
      </List>
    </CompactView>
  );
}

Vision.render(<Overlay />, { target: "layer" });

Interactive Page

An extension that uses the interactive target for participant-facing controls during a stream session. What it does: Editor sets the default message. Interactive page has buttons to trigger effects. Layer displays the current state. Use this pattern for active participants and operators, not as a high-scale public page for thousands of viewers.

Editor — src/admin.tsx

import {
  Vision,
  CompactView,
  Text,
  TextField,
  useExtensionStorage,
} from "@1upvision/sdk";

function Settings() {
  const [storage, setStorage] = useExtensionStorage();

  return (
    <CompactView title="Alert Extension">
      <Text content="Set the default alert message." variant="muted" />
      <TextField
        label="Alert Text"
        placeholder="Something exciting happened!"
        value={storage.alertText ?? ""}
        onChange={(value) => setStorage({ ...storage, alertText: value })}
      />
    </CompactView>
  );
}

Vision.render(<Settings />, { target: "editor" });

Interactive — src/interactive.tsx

import {
  Vision,
  CompactView,
  Text,
  Button,
  useExtensionStorage,
} from "@1upvision/sdk";

function Interactive() {
  const [storage, setStorage] = useExtensionStorage();

  return (
    <CompactView title="Alert Controls">
      <Text content="Trigger alerts during the stream." variant="muted" />
      <Button
        label="Show Alert"
        onClick={() =>
          setStorage({ ...storage, showAlert: true, alertTime: Date.now() })
        }
      />
      <Button
        label="Hide Alert"
        variant="outline"
        onClick={() => setStorage({ ...storage, showAlert: false })}
      />
    </CompactView>
  );
}

Vision.render(<Interactive />, { target: "interactive" });

Layer — src/layer.tsx

import { Vision, Text, useExtensionStorage } from "@1upvision/sdk";

function Overlay() {
  const [storage] = useExtensionStorage();

  if (!storage.showAlert) return null;

  return <Text content={storage.alertText ?? "Alert!"} variant="heading" />;
}

Vision.render(<Overlay />, { target: "layer" });

Leaderboard (Server Functions)

A full-stack example using server functions. Players are stored in an edge database — the editor manages players, the layer shows the leaderboard on stream. What it does: Schema defines a players table. Server functions handle CRUD. Editor panel adds/removes players. Layer renders the top scores.

Schema — server/schema.ts

import { defineSchema, defineTable, v } from "@1upvision/sdk/server";

export default defineSchema({
  players: defineTable({
    name: v.string(),
    score: v.number(),
  }).index("by_score", ["score"]),
});

Functions — server/functions.ts

import { query, mutation, v } from "@1upvision/sdk/server";

export const getLeaderboard = query({
  args: {
    limit: v.number().optional(),
  },
  handler: async (ctx, args) => {
    return await ctx.db.query("players", {
      index: { name: "by_score", values: [] },
      limit: args.limit ?? 10,
    });
  },
});

export const addPlayer = mutation({
  args: {
    name: v.string(),
    score: v.number(),
  },
  handler: async (ctx, args) => {
    return await ctx.db.insert("players", {
      name: args.name,
      score: args.score,
    });
  },
});

export const updateScore = mutation({
  args: {
    id: v.string(),
    score: v.number(),
  },
  handler: async (ctx, args) => {
    await ctx.db.patch("players", args.id, { score: args.score });
  },
});

export const removePlayer = mutation({
  args: {
    id: v.string(),
  },
  handler: async (ctx, args) => {
    await ctx.db.delete("players", args.id);
  },
});

Editor — src/admin.tsx

import { useState } from "react";
import {
  Vision,
  CompactView,
  Text,
  TextField,
  NumberField,
  Button,
  List,
  Box,
  useQuery,
  useMutation,
} from "@1upvision/sdk";

function Settings() {
  const { data: players, isLoading } = useQuery("getLeaderboard");
  const { mutate: addPlayer } = useMutation("addPlayer");
  const { mutate: removePlayer } = useMutation("removePlayer");
  const [name, setName] = useState("");
  const [score, setScore] = useState(0);

  return (
    <CompactView title="Leaderboard">
      <Text content="Add players and manage scores." variant="muted" />
      <List gap={12}>
        <TextField
          label="Player Name"
          placeholder="Enter name..."
          value={name}
          onChange={setName}
        />
        <NumberField
          label="Starting Score"
          value={score}
          min={0}
          onChange={setScore}
        />
        <Button
          label="Add Player"
          onClick={() => {
            addPlayer({ name, score });
            setName("");
            setScore(0);
          }}
        />
      </List>

      {isLoading ? (
        <Text content="Loading..." variant="muted" />
      ) : (
        <List gap={4}>
          {((players as any[]) ?? []).map((player: any) => (
            <Box
              key={player.id}
              style={{
                display: "flex",
                justifyContent: "space-between",
                alignItems: "center",
              }}
            >
              <Text content={`${player.name}${player.score}`} />
              <Button
                label="Remove"
                variant="destructive"
                onClick={() => removePlayer({ id: player.id })}
              />
            </Box>
          ))}
        </List>
      )}
    </CompactView>
  );
}

Vision.render(<Settings />, { target: "editor" });

Layer — src/layer.tsx

import { Vision, CompactView, List, Text, useQuery } from "@1upvision/sdk";

function Overlay() {
  const { data: players } = useQuery("getLeaderboard");

  return (
    <CompactView>
      <Text content="Leaderboard" variant="heading" />
      <List gap={4}>
        {((players as any[]) ?? []).map((player: any, i: number) => (
          <Text
            key={player.id}
            content={`${i + 1}. ${player.name}${player.score}`}
          />
        ))}
      </List>
    </CompactView>
  );
}

Vision.render(<Overlay />, { target: "layer" });