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.jsonThis 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.