Documentation
Build server-rendered Fireclass models, typed Server Actions, and client-safe data flow in Next.js.
This guide builds a posts workflow with Firebase Admin reads in a Server Component, validated writes in Server Actions, and plain serialized data across the React Server Component boundary.
Example application
Browse the Fireclass Next.js example for the generated App Router project structure and configuration.
app/
actions.ts
new-post-form.tsx
page.tsx
post-list.tsx
lib/
fireclass.server.ts
models/
post.ts
types/
post.ts
.env.local
fireclass.json
tsconfig.jsonStart from the Next.js setup, then verify the project:
npx fireclass doctorFIREBASE_PROJECT_ID=your-project-id
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@your-project-id.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"getFireclass() also accepts explicit options or Application Default
Credentials. Do not prefix Admin values with NEXT_PUBLIC_.
No firebase.ts file
The SSR package initializes and memoizes Firebase Admin. A separate Firebase entry is not recommended for this runtime.
import "reflect-metadata";
import "server-only";
import { getFireclass } from "@dharayush7/fireclass-ssr";
export const { BaseModel, adapter } = getFireclass();The server-only guard converts an accidental Client Component import into a
build error.
Client Components should not import the server model, even only to obtain its type. Keep the serializable field contract in a neutral file.
export interface PostShape {
id?: string;
title: string;
body: string;
createdAt: Date;
}import "server-only";
import { Collection } from "@dharayush7/fireclass-ssr";
import { Type } from "class-transformer";
import { IsDate, IsString, Length } from "class-validator";
import { BaseModel } from "@/lib/fireclass.server";
import type { PostShape } from "@/types/post";
@Collection("posts")
export class Post extends BaseModel<Post> implements PostShape {
@IsString()
@Length(1, 120)
title!: string;
@IsString()
@Length(1, 1000)
body!: string;
@IsDate()
@Type(() => Date)
createdAt!: Date;
constructor(data?: Partial<Post>) {
super(data);
Object.assign(this, data);
}
}The decorator comes directly from fireclass-ssr; the initialized local entry
exports only BaseModel and adapter.
Firestore reads happen on the server. Serialize model instances before passing them to a Client Component.
import { serializeList } from "@dharayush7/fireclass-ssr";
import { Post } from "@/models/post";
import type { PostShape } from "@/types/post";
import { NewPostForm } from "./new-post-form";
import { PostList } from "./post-list";
export const dynamic = "force-dynamic";
export default async function Page() {
const posts = await Post.findMany({
orderBy: { createdAt: "desc" },
limit: 50,
});
return (
<main>
<h1>Posts</h1>
<NewPostForm />
<PostList initial={serializeList<PostShape>(posts)} />
</main>
);
}force-dynamic makes the example read Firestore for each request. In a larger
application, choose an explicit caching and revalidation policy for each route.
runAction keeps validation failures inside a typed result. serialize
converts the saved model and its Date value into an RSC-safe payload.
"use server";
import {
runAction,
serialize,
type ActionResult,
} from "@dharayush7/fireclass-ssr";
import type {
Serialized,
} from "@dharayush7/fireclass-ssr/client";
import { revalidatePath } from "next/cache";
import { Post } from "@/models/post";
import type { PostShape } from "@/types/post";
type CreatePostResult = ActionResult<Serialized<PostShape>>;
export async function createPost(
_previous: CreatePostResult | null,
formData: FormData,
): Promise<CreatePostResult> {
const result = await runAction(async () => {
const post = new Post({
title: String(formData.get("title") ?? ""),
body: String(formData.get("body") ?? ""),
createdAt: new Date(),
});
await post.save();
return serialize<PostShape>(post);
});
if (result.ok) revalidatePath("/");
return result;
}
export async function deletePost(id: string): Promise<void> {
await Post.deleteById(id);
revalidatePath("/");
}Non-Fireclass errors still throw and reach the nearest Next.js error boundary.
"use client";
import { useActionState } from "react";
import { createPost } from "./actions";
export function NewPostForm() {
const [state, action, pending] = useActionState(createPost, null);
return (
<form action={action}>
<label htmlFor="title">Title</label>
<input id="title" name="title" maxLength={120} />
<label htmlFor="body">Body</label>
<textarea id="body" name="body" maxLength={1000} rows={4} />
<button type="submit" disabled={pending}>
{pending ? "Saving..." : "Add post"}
</button>
{state && !state.ok && (
<div role="alert">
<p>{state.error}</p>
{state.details && (
<ul>
{state.details.map((detail) => (
<li key={detail.property}>
{detail.property}: {Object.values(detail.constraints).join(", ")}
</li>
))}
</ul>
)}
</div>
)}
</form>
);
}Import only the client-safe SSR subpath in the Client Component. reviveDates
converts selected top-level ISO strings back into Date instances.
"use client";
import {
reviveDates,
type Serialized,
} from "@dharayush7/fireclass-ssr/client";
import type { PostShape } from "@/types/post";
import { deletePost } from "./actions";
export function PostList({
initial,
}: {
initial: Serialized<PostShape>[];
}) {
if (initial.length === 0) return <p>No posts yet.</p>;
return (
<ul>
{initial.map((raw) => {
const post = reviveDates(raw, ["createdAt"]);
return (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
<time dateTime={post.createdAt.toISOString()}>
{post.createdAt.toLocaleString()}
</time>
<form action={deletePost.bind(null, String(post.id))}>
<button type="submit">Delete</button>
</form>
</li>
);
})}
</ul>
);
}npm run buildThe build should succeed without placing firebase-admin in a client chunk. A
Client Component import from the root @dharayush7/fireclass-ssr entry should
fail because that entry is server-only.
getFireclass, and root SSR imports in server-only modules.revalidatePath or
revalidateTag after mutations.npx fireclass doctor, typecheck, and next build in CI.See the SSR API reference for credential priority, singleton scope, and serialization signatures.