LogoBetaFlow

Get Started

How to start using betaflow in your app.

Quick Start

Run the following command to add the Betaflow editor to your project:

npx shadcn@latest add https://betaflow-v1.vercel.app/r/editor.json

This installs all required packages and adds the editor to your components/editor directory.


Usage

Import and render the editor in any page or component:

import { Editor } from "@/components/editor";

export default function Page() {
  return <Editor content={blog.content} />;
}

Syntax Highlighting

If your content includes code blocks, you'll need to import a highlight.js stylesheet. Add the following to your global CSS file:

@import "highlight.js/styles/github.css";

Dark Mode

To support dark mode, add a scoped import using a custom variant. This ensures the dark theme only activates when the .dark class is present:

@custom-variant dark (&:is(.dark *));

.dark {
  @import "highlight.js/styles/atom-one-dark.css";
}

Make sure highlight.js is listed as a dependency in your registry item, or install it manually with npm install highlight.js.


Fetching example

Use https://famous-mongoose-363.convex.site as NEXT_PUBLIC_BETAFLOW_API_URL.

For BETAFLOW_API_KEY , you can create it in settings of your betaflow account.

Get my user data

import type { TUser } from "@/types";
//from docs/types

interface RateLimitInfo {
  limit: number;
  plan: string;
  remaining: number;
  reset: string;
  used: number;
}

function extractRateLimit(headers: Headers): RateLimitInfo {
  return {
    limit: Number(headers.get("X-RateLimit-Limit") ?? 0),
    remaining: Number(headers.get("X-RateLimit-Remaining") ?? 0),
    used: Number(headers.get("X-RateLimit-Used") ?? 0),
    reset: headers.get("X-RateLimit-Reset") ?? "",
    plan: headers.get("X-RateLimit-Plan") ?? "free",
  };
}

export interface UserResponse {
  data: TUser | null;
  rateLimit: RateLimitInfo;
}

export async function getMe(): Promise<UserResponse> {
  const baseUrl = process.env.NEXT_PUBLIC_BETAFLOW_API_URL;
  const apiKey = process.env.BETAFLOW_API_KEY;

  if (!baseUrl) {
    throw new Error("NEXT_PUBLIC_BETAFLOW_API_URL is not set");
  }
  if (!apiKey) {
    throw new Error("BETAFLOW_API_KEY is not set");
  }

  let res: Response;
  try {
    res = await fetch(`${baseUrl}/getMe`, {
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      next: { revalidate: 60 },
    });
  } catch (err) {
    throw new Error(
      `Network error while fetching user: ${err instanceof Error ? err.message : String(err)}`,
    );
  }

  // Handle rate limit exceeded
  if (res.status === 429) {
    const rateLimit = extractRateLimit(res.headers);
    throw new Error(
      `Rate limit exceeded. Plan: ${rateLimit.plan} (${rateLimit.limit}/month). Resets at ${rateLimit.reset}`,
    );
  }

  if (res.status === 404) {
    return {
      data: null,
      rateLimit: extractRateLimit(res.headers),
    };
  }

  if (!res.ok) {
    const errorBody = await res.text().catch(() => "");
    throw new Error(
      `Failed to fetch user: ${res.status} ${res.statusText}${errorBody ? ` — ${errorBody}` : ""}`,
    );
  }

  let body: unknown;
  try {
    body = await res.json();
  } catch {
    throw new Error("Failed to parse user response as JSON");
  }

  if (typeof body !== "object" || body === null || !("data" in body)) {
    throw new Error(
      `Unexpected response shape: expected { data: {} }, got ${typeof body}`,
    );
  }

  return {
    data: (body as { data: TUser }).data ?? null,
    rateLimit: extractRateLimit(res.headers),
  };
}

Display User info


export async function UserAbout() {
  const { data: user, rateLimit } = await getMe();

  if (user === null) {
    return (
      <section className="relative">
        <p>User not found</p>
      </section>
    );
  }
  return (
    <section className="relative">
      <div className="flex flex-col gap-5">
        <Link href={`/user/${user.username}`}>
          <Avatar className="aspect-square size-16 rounded-full">
            <AvatarImage src={user.image || profileImg.src} />
            <AvatarFallback>{getUserInitials(user.name)}</AvatarFallback>
          </Avatar>
        </Link>
        <h1>{user.name}</h1>
        {user.publicInfo?.bio && (
          <p>{user.publicInfo.bio}</p>
        )}

        <p className="text-muted-foreground text-xs">
          API Rate Limit: {rateLimit.used} / {rateLimit.limit} (remaining:{" "}
          {rateLimit.remaining}, resets at{" "}
          {format(rateLimit.reset, "MMM d, yyyy h:mm a")})
        </p>
      </div>
    </section>
  );
}

Get my blogs

import type { TBlog } from "@/types";
//from docs/types

interface RateLimitInfo {
  limit: number;
  plan: string;
  remaining: number;
  reset: string;
  used: number;
}

interface BlogsResponse {
  data: TBlog[];
  rateLimit: RateLimitInfo;
}

function extractRateLimit(headers: Headers): RateLimitInfo {
  return {
    limit: Number(headers.get("X-RateLimit-Limit") ?? 0),
    remaining: Number(headers.get("X-RateLimit-Remaining") ?? 0),
    used: Number(headers.get("X-RateLimit-Used") ?? 0),
    reset: headers.get("X-RateLimit-Reset") ?? "",
    plan: headers.get("X-RateLimit-Plan") ?? "free",
  };
}

export async function getMyBlogs(): Promise<BlogsResponse> {
  const baseUrl = process.env.NEXT_PUBLIC_BETAFLOW_API_URL;
  const apiKey = process.env.BETAFLOW_API_KEY;

  if (!baseUrl) {
    throw new Error("NEXT_PUBLIC_BETAFLOW_API_URL is not set");
  }
  if (!apiKey) {
    throw new Error("BETAFLOW_API_KEY is not set");
  }

  let res: Response;
  try {
    res = await fetch(`${baseUrl}/getMyBlogs`, {
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      next: { revalidate: 60 },
    });
  } catch (err) {
    throw new Error(
      `Network error while fetching blogs: ${err instanceof Error ? err.message : String(err)}`,
    );
  }

  // Handle rate limit exceeded specifically
  if (res.status === 429) {
    const rateLimit = extractRateLimit(res.headers);
    throw new Error(
      `Rate limit exceeded. Plan: ${rateLimit.plan} (${rateLimit.limit}/month). Resets at ${rateLimit.reset}`,
    );
  }

  if (res.status === 404) {
    return {
      data: [],
      rateLimit: extractRateLimit(res.headers),
    };
  }

  if (!res.ok) {
    const errorBody = await res.text().catch(() => "");
    throw new Error(
      `Failed to fetch blogs: ${res.status} ${res.statusText}${errorBody ? ` — ${errorBody}` : ""}`,
    );
  }

  let body: unknown;
  try {
    body = await res.json();
  } catch {
    throw new Error("Failed to parse blog response as JSON");
  }

  if (
    typeof body !== "object" ||
    body === null ||
    !("data" in body) ||
    !Array.isArray((body as { data: unknown }).data)
  ) {
    throw new Error(
      `Unexpected response shape: expected { data: [] }, got ${typeof body}`,
    );
  }

  return {
    data: (body as { data: TBlog[] }).data,
    rateLimit: extractRateLimit(res.headers),
  };
}

Displaying blogs

export default async function UserBlogs() {
  const { data: blogs } = await getMyBlogs();

  const categoryMap = new Map<string, { title: string; slug: string }>();

  categoryMap.set("all", { title: "All Blogs", slug: "all" });

  for (const blog of blogs) {
    if (blog.group?.slug) {
      categoryMap.set(blog.group.slug, {
        title: blog.group.title ?? "",
        slug: blog.group.slug,
      });
    }
  }

  if (blogs.length === 0 || user === null) {
    return null;
  }

  const categories = Array.from(categoryMap.values());

  return (
    <>
      <BlogsCategories categories={categories} />
      <BlogCards blogs={blogs}  />
    </>
  );
}

Get my files

import type { TFile } from "@/types";
// from docs/types

interface RateLimitInfo {
  limit: number;
  plan: string;
  remaining: number;
  reset: string;
  used: number;
}

function extractRateLimit(headers: Headers): RateLimitInfo {
  return {
    limit: Number(headers.get("X-RateLimit-Limit") ?? 0),
    remaining: Number(headers.get("X-RateLimit-Remaining") ?? 0),
    used: Number(headers.get("X-RateLimit-Used") ?? 0),
    reset: headers.get("X-RateLimit-Reset") ?? "",
    plan: headers.get("X-RateLimit-Plan") ?? "free",
  };
}

interface FilesResponse {
  data: TFile[];
  rateLimit: RateLimitInfo;
}

export async function getMyFiles(): Promise<FilesResponse> {
  const baseUrl = process.env.NEXT_PUBLIC_BETAFLOW_API_URL;
  const apiKey = process.env.BETAFLOW_API_KEY;

  if (!baseUrl) {
    throw new Error("NEXT_PUBLIC_BETAFLOW_API_URL is not set");
  }
  if (!apiKey) {
    throw new Error("BETAFLOW_API_KEY is not set");
  }

  let res: Response;
  try {
    res = await fetch(`${baseUrl}/getMyFiles`, {
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      next: { revalidate: 60 },
    });
  } catch (err) {
    throw new Error(
      `Network error while fetching files: ${err instanceof Error ? err.message : String(err)}`,
    );
  }

  if (res.status === 429) {
    const rateLimit = extractRateLimit(res.headers);
    throw new Error(
      `Rate limit exceeded. Plan: ${rateLimit.plan} (${rateLimit.limit}/month). Resets at ${rateLimit.reset}`,
    );
  }

  if (res.status === 404) {
    return {
      data: [],
      rateLimit: extractRateLimit(res.headers),
    };
  }

  if (!res.ok) {
    const errorBody = await res.text().catch(() => "");
    throw new Error(
      `Failed to fetch files: ${res.status} ${res.statusText}${errorBody ? ` — ${errorBody}` : ""}`,
    );
  }

  let body: unknown;
  try {
    body = await res.json();
  } catch {
    throw new Error("Failed to parse files response as JSON");
  }

  if (
    typeof body !== "object" ||
    body === null ||
    !("data" in body) ||
    !Array.isArray((body as { data: unknown }).data)
  ) {
    throw new Error(
      `Unexpected response shape: expected { data: [] }, got ${typeof body}`,
    );
  }

  return {
    data: (body as { data: TFile[] }).data,
    rateLimit: extractRateLimit(res.headers),
  };
}

Displaying files

export default async function UserFiles() {
  const { data: files } = await getMyFiles();

  const categoryMap = new Map<string, { title: string; slug: string }>();

  categoryMap.set("all", { title: "All Files", slug: "all" });

  for (const file of files) {
    if (file.group?.slug) {
      categoryMap.set(file.group.slug, {
        title: file.group.title ?? "",
        slug: file.group.slug,
      });
    }
  }

  if (files.length === 0) {
    return null;
  }

  const categories = Array.from(categoryMap.values());

  return (
    <>
      <FilesCategories categories={categories} />
      <FileCards files={files} />
    </>
  );
}

Starter Template

Get up and running quickly with our official starter template, which includes a pre-configured editor, routing, and styling.

View on GitHub →

On this page