Backend-as-a-Service & Database Solutions: Building Modern Full-Stack Applications
The backend development landscape has undergone a revolutionary transformation with the emergence of integrated Backend-as-a-Service (BaaS) platforms that combine real-time databases, authentication, serverless functions, and edge computing into unified development experiences. This comprehensive guide explores the cutting-edge technologies that are eliminating backend complexity while delivering unprecedented performance and developer productivity.
Executive Summary
Modern full-stack development in 2025 is defined by the rise of integrated backend solutions that eliminate the complexity of managing separate services for databases, authentication, real-time updates, and API endpoints. Traditional backend development required orchestrating multiple services—a database server, authentication provider, API layer, caching system, and message queue—each with its own configuration, scaling challenges, and maintenance overhead.
Today's BaaS platforms represent a paradigm shift by providing these capabilities as unified, TypeScript-first development experiences. Convex leads this revolution with reactive database queries that automatically update all connected clients when data changes, eliminating manual cache invalidation and state management complexity. Supabase brings the power of PostgreSQL to the serverless world with built-in real-time subscriptions, row-level security, and auto-generated APIs. Better Auth provides comprehensive authentication with 350,000+ weekly npm downloads, supporting everything from basic email/password to advanced enterprise SSO and passkey authentication.
These platforms share common characteristics that define modern backend development: instant deployment without infrastructure configuration, automatic scaling from zero to millions of users, built-in security and compliance features, comprehensive developer tooling, and seamless integration with modern frontend frameworks like Next.js, React, and Vue. The result is 10x faster backend development with production-ready infrastructure that handles complexity automatically while maintaining the flexibility to implement custom business logic.
Technical Deep Dive
Convex: Revolutionary Reactive Backend Platform
Convex represents the next evolution in backend architecture with its reactive, TypeScript-first approach. Unlike traditional databases that require manual cache invalidation and complex state management, Convex automatically tracks query dependencies and updates all connected clients in real-time when data changes.
Core Architecture:
// Define schema with TypeScript
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
tokenIdentifier: v.string(),
imageUrl: v.optional(v.string()),
}).index("by_token", ["tokenIdentifier"]),
posts: defineTable({
title: v.string(),
content: v.string(),
authorId: v.id("users"),
published: v.boolean(),
tags: v.array(v.string()),
createdAt: v.number(),
})
.index("by_author", ["authorId"])
.index("by_published", ["published", "createdAt"]),
});
Reactive Queries:
// queries/posts.ts - Automatically reactive
import { query } from "./_generated/server";
import { v } from "convex/values";
export const getPublishedPosts = query({
args: { limit: v.optional(v.number()) },
handler: async (ctx, args) => {
const posts = await ctx.db
.query("posts")
.withIndex("by_published", (q) =>
q.eq("published", true)
)
.order("desc")
.take(args.limit ?? 20);
// Automatically fetch related user data
return await Promise.all(
posts.map(async (post) => ({
...post,
author: await ctx.db.get(post.authorId),
}))
);
},
});
// React component - automatically updates when data changes
function PostsList() {
const posts = useQuery(api.posts.getPublishedPosts, { limit: 10 });
if (posts === undefined) return ;
return (
{posts.map((post) => (
))}
);
}
Mutations and Transactions:
// mutations/posts.ts - Atomic operations with ACID guarantees
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const createPost = mutation({
args: {
title: v.string(),
content: v.string(),
tags: v.array(v.string()),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthorized");
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (!user) throw new Error("User not found");
// Insert with transaction guarantees
const postId = await ctx.db.insert("posts", {
title: args.title,
content: args.content,
authorId: user._id,
published: false,
tags: args.tags,
createdAt: Date.now(),
});
return postId;
},
});
Scheduled Functions & Cron Jobs:
// crons.ts - Built-in task scheduling
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
// Run daily cleanup at 2 AM UTC
crons.daily(
"delete old drafts",
{ hourUTC: 2, minuteUTC: 0 },
internal.maintenance.deleteOldDrafts
);
// Process pending notifications every 5 minutes
crons.interval(
"process notifications",
{ minutes: 5 },
internal.notifications.processQueue
);
export default crons;
Better Auth: Comprehensive TypeScript Authentication
Better Auth has emerged as the most comprehensive authentication framework for TypeScript in 2025, with 350,000+ weekly npm downloads and endorsements from Next.js, Nuxt, and Astro core teams.
Core Setup:
// lib/auth.ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { openAPI } from "better-auth/plugins";
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "postgresql",
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
plugins: [
openAPI(), // Auto-generate OpenAPI docs
],
});
Advanced Authentication Patterns:
// Two-Factor Authentication
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
// ... base config
plugins: [
twoFactor({
issuer: "MyApp",
totpWindow: 1, // Allow 1 time-step tolerance
}),
],
});
// Passkey (WebAuthn) Support
import { passkey } from "better-auth/plugins";
export const auth = betterAuth({
// ... base config
plugins: [
passkey({
rpName: "MyApp",
rpID: "myapp.com",
}),
],
});
// Multi-tenant Authentication
import { multiTenant } from "better-auth/plugins";
export const auth = betterAuth({
// ... base config
plugins: [
multiTenant({
tenantIdField: "organizationId",
}),
],
});
Client Integration:
// Client-side usage with React
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
});
// Sign-in component
function SignIn() {
const { signIn, isPending } = authClient.useSignIn();
const handleSignIn = async (email: string, password: string) => {
await signIn.email({
email,
password,
callbackURL: "/dashboard",
});
};
return (
);
}
// Session management
function UserProfile() {
const { data: session } = authClient.useSession();
if (!session) return ;
return (
Welcome, {session.user.name}
);
}
Supabase: PostgreSQL-Powered BaaS Platform
Supabase provides a complete PostgreSQL database with built-in real-time subscriptions, authentication, storage, and edge functions—all accessible through auto-generated APIs.
Database Setup with Row-Level Security:
-- Create tables with RLS policies
CREATE TABLE profiles (
id UUID REFERENCES auth.users PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Policy: Users can view all profiles
CREATE POLICY "Profiles are viewable by everyone"
ON profiles FOR SELECT
TO authenticated
USING (true);
-- Policy: Users can update their own profile
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
TO authenticated
USING (auth.uid() = id);
-- Create posts with author relationship
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
author_id UUID REFERENCES profiles(id) NOT NULL,
published BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policy: Published posts viewable by all, drafts only by author
CREATE POLICY "Published posts are viewable by everyone"
ON posts FOR SELECT
TO authenticated
USING (published = true OR auth.uid() = author_id);
Real-Time Subscriptions:
// Real-time data updates
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// Subscribe to database changes
function RealtimePostsList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
// Fetch initial data
const fetchPosts = async () => {
const { data } = await supabase
.from('posts')
.select('*, profiles(username, avatar_url)')
.eq('published', true)
.order('created_at', { ascending: false });
if (data) setPosts(data);
};
fetchPosts();
// Subscribe to real-time changes
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE
schema: 'public',
table: 'posts',
filter: 'published=eq.true',
},
(payload) => {
if (payload.eventType === 'INSERT') {
setPosts((current) => [payload.new as Post, ...current]);
} else if (payload.eventType === 'UPDATE') {
setPosts((current) =>
current.map((post) =>
post.id === payload.new.id ? (payload.new as Post) : post
)
);
} else if (payload.eventType === 'DELETE') {
setPosts((current) =>
current.filter((post) => post.id !== payload.old.id)
);
}
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, []);
return (
{posts.map((post) => (
))}
);
}
Edge Functions:
// supabase/functions/process-payment/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import Stripe from "https://esm.sh/stripe@12.0.0?target=deno";
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, {
apiVersion: '2023-10-16',
});
serve(async (req) => {
const { amount, currency, customerId } = await req.json();
try {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency,
customer: customerId,
automatic_payment_methods: { enabled: true },
});
return new Response(
JSON.stringify({ clientSecret: paymentIntent.client_secret }),
{ headers: { "Content-Type": "application/json" } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
});
Storage Integration:
// File upload with storage policies
const uploadAvatar = async (file: File, userId: string) => {
const fileExt = file.name.split('.').pop();
const fileName = ${userId}-${Date.now()}.${fileExt}
;
const filePath = avatars/${fileName}
;
// Upload to Supabase Storage
const { data, error } = await supabase.storage
.from('public')
.upload(filePath, file, {
cacheControl: '3600',
upsert: false,
});
if (error) throw error;
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('public')
.getPublicUrl(filePath);
// Update user profile
await supabase
.from('profiles')
.update({ avatar_url: publicUrl })
.eq('id', userId);
return publicUrl;
};
Integration Solutions
Better T Stack (Convex + Better Auth + TanStack):
// Convex auth integration with Better Auth
import { convexAuth } from "@convex-dev/auth/server";
import { betterAuthAdapter } from "@convex-dev/better-auth";
export const { auth, signIn, signOut, store } = convexAuth({
providers: [
betterAuthAdapter({
// Better Auth configuration
}),
],
});
// TanStack Query integration
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
const queryClient = new QueryClient();
function App() {
return (
);
}
Real-World Examples
E-Commerce Platform with Real-Time Inventory
// Convex schema for e-commerce
export default defineSchema({
products: defineTable({
name: v.string(),
description: v.string(),
price: v.number(),
inventory: v.number(),
sku: v.string(),
}).index("by_sku", ["sku"]),
orders: defineTable({
userId: v.id("users"),
items: v.array(v.object({
productId: v.id("products"),
quantity: v.number(),
price: v.number(),
})),
status: v.union(
v.literal("pending"),
v.literal("processing"),
v.literal("shipped"),
v.literal("delivered")
),
total: v.number(),
createdAt: v.number(),
}).index("by_user", ["userId"]),
});
// Mutation with inventory management
export const createOrder = mutation({
args: {
items: v.array(v.object({
productId: v.id("products"),
quantity: v.number(),
})),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthorized");
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (!user) throw new Error("User not found");
// Validate inventory and calculate total
let total = 0;
const orderItems = [];
for (const item of args.items) {
const product = await ctx.db.get(item.productId);
if (!product) throw new Error(Product ${item.productId} not found
);
if (product.inventory < item.quantity) {
throw new Error(Insufficient inventory for ${product.name}
);
}
// Reserve inventory
await ctx.db.patch(item.productId, {
inventory: product.inventory - item.quantity,
});
orderItems.push({
productId: item.productId,
quantity: item.quantity,
price: product.price,
});
total += product.price * item.quantity;
}
// Create order
const orderId = await ctx.db.insert("orders", {
userId: user._id,
items: orderItems,
status: "pending",
total,
createdAt: Date.now(),
});
return orderId;
},
});
// Real-time inventory display
function ProductCard({ productId }: { productId: Id<"products"> }) {
const product = useQuery(api.products.get, { productId });
if (!product) return ;
const isLowStock = product.inventory < 10;
const isOutOfStock = product.inventory === 0;
return (
{product.name}
${product.price.toFixed(2)}
{isOutOfStock
? "Out of Stock"
: ${product.inventory} in stock${isLowStock ? " - Low Stock!" : ""}
}
);
}
Collaborative Document Editor
// Supabase real-time collaboration
interface Document {
id: string;
title: string;
content: string;
owner_id: string;
collaborators: string[];
last_edited_by: string;
updated_at: string;
}
function CollaborativeEditor({ documentId }: { documentId: string }) {
const [document, setDocument] = useState(null);
const [activeUsers, setActiveUsers] = useState([]);
useEffect(() => {
// Fetch document
const fetchDocument = async () => {
const { data } = await supabase
.from('documents')
.select('*')
.eq('id', documentId)
.single();
if (data) setDocument(data);
};
fetchDocument();
// Subscribe to document changes
const documentChannel = supabase
.channel(document:${documentId}
)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'documents',
filter: id=eq.${documentId}
,
},
(payload) => {
setDocument(payload.new as Document);
}
)
.on('presence', { event: 'sync' }, () => {
const state = documentChannel.presenceState();
setActiveUsers(Object.keys(state));
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await documentChannel.track({ user_id: user.id });
}
});
return () => {
documentChannel.unsubscribe();
};
}, [documentId]);
const handleContentChange = async (newContent: string) => {
// Optimistic update
setDocument((prev) => prev ? { ...prev, content: newContent } : null);
// Persist to database
await supabase
.from('documents')
.update({
content: newContent,
last_edited_by: user.id,
updated_at: new Date().toISOString(),
})
.eq('id', documentId);
};
return (
{document?.title}
{activeUsers.map((userId) => (
))}
);
}
Multi-Tenant SaaS Application
// Better Auth multi-tenant setup
import { betterAuth } from "better-auth";
import { multiTenant, admin } from "better-auth/plugins";
export const auth = betterAuth({
database: prismaAdapter(prisma),
plugins: [
multiTenant({
tenantIdField: "organizationId",
}),
admin(), // Admin role management
],
});
// Organization-scoped data access
async function getOrganizationData(organizationId: string) {
const { data, error } = await supabase
.from('analytics')
.select('*')
.eq('organization_id', organizationId);
return data;
}
// Role-based access control
function AdminPanel() {
const { data: session } = authClient.useSession();
if (!session?.user.role === 'admin') {
return ;
}
return (
);
}
Common Pitfalls
1. Over-Fetching Data in Reactive Systems
Problem:
// Bad: Fetching entire user objects when only names needed
export const getPosts = query({
handler: async (ctx) => {
const posts = await ctx.db.query("posts").collect();
return await Promise.all(
posts.map(async (post) => ({
...post,
author: await ctx.db.get(post.authorId), // Fetches all user fields
}))
);
},
});
Solution:
// Good: Select only required fields
export const getPosts = query({
handler: async (ctx) => {
const posts = await ctx.db.query("posts").collect();
return await Promise.all(
posts.map(async (post) => {
const author = await ctx.db.get(post.authorId);
return {
...post,
authorName: author?.name,
authorAvatar: author?.imageUrl,
};
})
);
},
});
2. Ignoring Real-Time Subscription Cleanup
Problem:
// Bad: Memory leak - subscription never cleaned up
function PostsList() {
const [posts, setPosts] = useState([]);
supabase
.channel('posts')
.on('postgres_changes', { /* ... */ }, (payload) => {
setPosts((current) => [...current, payload.new]);
})
.subscribe();
return
{/* render posts */};
}
Solution:
// Good: Proper cleanup with useEffect
function PostsList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
const channel = supabase
.channel('posts')
.on('postgres_changes', { /* ... */ }, (payload) => {
setPosts((current) => [...current, payload.new]);
})
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, []);
return
{/* render posts */};
}
3. Incorrect Row-Level Security Policies
Problem:
-- Bad: Allows users to update other users' data
CREATE POLICY "Users can update profiles"
ON profiles FOR UPDATE
TO authenticated
USING (true); -- No restriction!
Solution:
-- Good: Restrict updates to own profile
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
TO authenticated
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);
4. Missing Error Handling in Mutations
Problem:
// Bad: No error handling
export const updateUser = mutation({
args: { name: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
await ctx.db.patch(user._id, { name: args.name });
},
});
Solution:
// Good: Comprehensive error handling
export const updateUser = mutation({
args: { name: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Unauthorized: User not authenticated");
}
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (!user) {
throw new Error("User not found");
}
if (!args.name.trim()) {
throw new Error("Name cannot be empty");
}
await ctx.db.patch(user._id, { name: args.name.trim() });
return { success: true };
},
});
Best Practices
1. Optimize Query Performance
// Use indexes for frequently queried fields
export default defineSchema({
posts: defineTable({
title: v.string(),
authorId: v.id("users"),
published: v.boolean(),
createdAt: v.number(),
})
.index("by_author", ["authorId"]) // Fast author lookups
.index("by_published", ["published", "createdAt"]) // Fast published posts query
.index("by_author_published", ["authorId", "published"]), // Combined queries
});
// Use pagination for large datasets
export const getPostsPaginated = query({
args: {
cursor: v.optional(v.string()),
limit: v.number(),
},
handler: async (ctx, args) => {
let query = ctx.db
.query("posts")
.withIndex("by_published", (q) => q.eq("published", true))
.order("desc");
if (args.cursor) {
query = query.filter((q) => q.lt(q.field("_creationTime"), args.cursor));
}
const posts = await query.take(args.limit + 1);
const hasMore = posts.length > args.limit;
const items = hasMore ? posts.slice(0, -1) : posts;
return {
items,
nextCursor: hasMore ? items[items.length - 1]._creationTime : null,
};
},
});
2. Implement Proper Authentication Patterns
// Middleware for protected routes
import { authMiddleware } from "better-auth/middleware";
export default authMiddleware({
publicRoutes: ["/", "/login", "/register"],
afterAuth: async (auth, req) => {
// Redirect unauthenticated users
if (!auth.userId && !auth.isPublicRoute) {
return Response.redirect(new URL("/login", req.url));
}
// Redirect authenticated users from auth pages
if (auth.userId && ["/login", "/register"].includes(req.nextUrl.pathname)) {
return Response.redirect(new URL("/dashboard", req.url));
}
},
});
// API route protection
export async function GET(req: Request) {
const session = await auth.getSession(req);
if (!session) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
// Proceed with authenticated request
const data = await fetchUserData(session.user.id);
return Response.json(data);
}
3. Design Efficient Database Schemas
-- Supabase: Use foreign keys and indexes
CREATE TABLE organizations (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
email TEXT UNIQUE NOT NULL,
role TEXT NOT NULL CHECK (role IN ('admin', 'member', 'viewer')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes for common queries
CREATE INDEX idx_users_org ON users(organization_id);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(organization_id, role);
4. Implement Robust Error Handling
// Centralized error handling
class AppError extends Error {
constructor(
public statusCode: number,
public message: string,
public isOperational = true
) {
super(message);
Object.setPrototypeOf(this, AppError.prototype);
}
}
// Mutation with error handling
export const processPayment = mutation({
args: {
orderId: v.id("orders"),
paymentMethodId: v.string(),
},
handler: async (ctx, args) => {
try {
const order = await ctx.db.get(args.orderId);
if (!order) {
throw new AppError(404, "Order not found");
}
if (order.status !== "pending") {
throw new AppError(400, "Order already processed");
}
// Process payment
const payment = await stripe.paymentIntents.create({
amount: order.total,
currency: "usd",
payment_method: args.paymentMethodId,
confirm: true,
});
// Update order
await ctx.db.patch(args.orderId, {
status: "processing",
paymentId: payment.id,
});
return { success: true, paymentId: payment.id };
} catch (error) {
if (error instanceof AppError) {
throw error;
}
// Log unexpected errors
console.error("Payment processing failed:", error);
throw new AppError(500, "Payment processing failed");
}
},
});
Integration Guidance
Convex with Next.js App Router
// app/providers.tsx
'use client';
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { ConvexAuthNextjsProvider } from "@convex-dev/auth/nextjs";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
// app/layout.tsx
import { Providers } from "./providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
Supabase with Expo React Native
// lib/supabase.ts
import 'react-native-url-polyfill/auto';
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL!,
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
{
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
}
);
// app/_layout.tsx
import { SessionProvider } from '@/components/SessionProvider';
export default function RootLayout() {
return (
);
}
Better Auth with tRPC
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { auth } from '@/lib/auth';
const t = initTRPC.context().create();
export const createContext = async ({ req, res }: CreateNextContextOptions) => {
const session = await auth.getSession(req);
return { req, res, session };
};
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
session: ctx.session,
},
});
});
export const protectedProcedure = t.procedure.use(isAuthed);
// server/routers/posts.ts
export const postsRouter = t.router({
create: protectedProcedure
.input(z.object({
title: z.string(),
content: z.string(),
}))
.mutation(async ({ ctx, input }) => {
return await prisma.post.create({
data: {
...input,
authorId: ctx.session.user.id,
},
});
}),
});
Getting Started
Quick Start with Convex
Install Convex
npm install convex
Initialize Convex project
npx convex dev
Deploy to production
npx convex deploy
Quick Start with Supabase
Install Supabase CLI
npm install -g supabase
Initialize project
supabase init
Start local development
supabase start
Link to cloud project
supabase link --project-ref your-project-ref
Quick Start with Better Auth
Install Better Auth
npm install better-auth
Generate auth schema
npx better-auth generate
Run migrations
npx prisma migrate dev
Conclusion
The modern BaaS landscape represents a fundamental shift in how we build backend systems. Platforms like Convex, Supabase, and Better Auth eliminate the complexity of managing separate services while providing enterprise-grade features like real-time synchronization, advanced authentication, and global edge deployment.
These technologies enable developers to build production-ready applications 10x faster than traditional backend development approaches. The reactive nature of Convex, combined with Supabase's PostgreSQL power and Better Auth's comprehensive authentication, provides a complete toolkit for modern full-stack development.
As applications increasingly demand real-time capabilities, global performance, and seamless user experiences, these BaaS platforms provide the infrastructure foundation for the next generation of web and mobile applications. The future of backend development is serverless, real-time, and developer-focused—and it's already here.