A modern, full-stack content creation and sharing platform built with Next.js 14, featuring a robust tRPC API, secure authentication, and real-time interactions.
Author: Abdallah Benassloun
WriteSpace is a comprehensive blogging and content sharing platform where writers can create, publish, and engage with stories. Built with modern web technologies, it showcases advanced patterns in full-stack TypeScript development, particularly focusing on type-safe APIs with tRPC.
Based on: This project was built using the Next.js Session Auth Template as a starting point, then extensively enhanced with additional features including social interactions, content management, and a complete UI redesign.
- Rich Markdown Editor with real-time preview
- Draft & Published States with seamless publishing workflow
- Public Content Discovery page for published stories
- Personal Dashboard for managing your content
- Nested Comments System with infinite pagination
- Like/Unlike functionality for posts and comments
- Real-time Interaction Counts with optimistic updates
- Community Engagement tools
- Lucia Auth integration with credential-based authentication
- Email Verification with secure token system
- Password Reset functionality
- Protected Routes and middleware
- Discord OAuth integration (configurable)
- Stripe Integration for premium features
- Webhook Handling for payment events
- Subscription Management dashboard
- Next.js 14 - React framework with App Router
- TypeScript - Type-safe development
- Tailwind CSS - Utility-first CSS framework
- Radix UI - Accessible component primitives
- React Hook Form - Form management
- React Markdown - Markdown rendering
- tRPC - End-to-end type-safe APIs
- Drizzle ORM - Type-safe database toolkit
- PostgreSQL - Robust relational database
- Lucia - Authentication library
- Zod - Schema validation
- Stripe - Payment processing
- React Email - Email template system
- Nodemailer - Email delivery
- Playwright - E2E testing
This project showcases advanced tRPC patterns and best practices:
The tRPC implementation follows a modular, organized structure that separates concerns and maintains type safety throughout:
src/
βββ server/api/
β βββ root.ts # Main app router - combines all feature routers
β βββ trpc.ts # tRPC configuration, context, and middleware
β βββ routers/
β βββ post/
β β βββ post.procedure.ts # tRPC procedures (queries & mutations)
β β βββ post.input.ts # Zod validation schemas
β β βββ post.service.ts # Business logic and database operations
β βββ comment/
β β βββ comment.procedure.ts
β β βββ comment.input.ts
β β βββ comment.service.ts
β βββ like/
β β βββ like.procedure.ts
β β βββ like.input.ts
β β βββ like.service.ts
β βββ user/
β β βββ user.procedure.ts
β β βββ user.input.ts
β β βββ user.service.ts
β βββ stripe/
β βββ stripe.procedure.ts
β βββ stripe.input.ts
β βββ stripe.service.ts
βββ trpc/
β βββ react.tsx # tRPC React Query client setup
β βββ server.ts # Server-side tRPC caller
β βββ shared.ts # Shared utilities and transformers
βββ app/api/trpc/[trpc]/
βββ route.ts # Next.js API route handler
/server/api/ - Core tRPC server implementation
root.ts: Combines all feature routers into the mainappRoutertrpc.ts: Contains context creation, middleware, and procedure definitions
/server/api/routers/{feature}/ - Feature-based organization
{feature}.procedure.ts: Defines tRPC procedures (queries/mutations){feature}.input.ts: Zod schemas for input validation{feature}.service.ts: Business logic separated from API layer
/trpc/ - Client-side tRPC configuration
react.tsx: React Query integration with tRPCserver.ts: Server-side tRPC calls for SSRshared.ts: Common utilities and type transformers
Client Request β tRPC Procedure β Input Validation β Service Layer β Database β Response
β β (Zod) (Business Logic) (Drizzle) β
React Component β Type-safe Response β Transformed Data β Database Result βββββββββββββ
// src/server/api/trpc.ts
export const createTRPCContext = async (opts: { headers: Headers }) => {
const { session, user } = await uncachedValidateRequest();
return {
session,
user,
db,
headers: opts.headers,
stripe: stripe,
};
};
// Protected procedure with guaranteed user context
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
session: { ...ctx.session },
user: { ...ctx.user },
},
});
});Each feature has its own router with input validation and service layer:
// src/server/api/routers/post/post.procedure.ts
export const postRouter = createTRPCRouter({
// Public endpoints
publicPosts: publicProcedure
.input(inputs.publicPostsSchema)
.query(({ ctx, input }) => services.getPublicPosts(ctx, input)),
// Protected endpoints
create: protectedProcedure
.input(inputs.createPostSchema)
.mutation(({ ctx, input }) => services.createPost(ctx, input)),
update: protectedProcedure
.input(inputs.updatePostSchema)
.mutation(({ ctx, input }) => services.updatePost(ctx, input)),
});// src/server/api/routers/post/post.input.ts
export const createPostSchema = z.object({
title: z.string().min(1, "Title is required").max(255),
excerpt: z.string().min(1, "Excerpt is required").max(255),
content: z.string().min(1, "Content is required"),
});
export const updatePostSchema = createPostSchema.extend({
id: z.string(),
status: z.enum(["draft", "published"]).default("draft"),
});// src/server/api/routers/post/post.service.ts
export const createPost = async (ctx: ProtectedTRPCContext, input: CreatePostInput) => {
const { id } = await ctx.db.insert(posts).values({
id: generateId(15),
userId: ctx.user.id,
title: input.title,
excerpt: input.excerpt,
content: input.content,
}).returning({ id: posts.id });
return { id };
};// Client-side usage with full type safety
const { data: posts, isLoading } = api.post.publicPosts.useQuery({
page: 1,
perPage: 12,
});
const createPost = api.post.create.useMutation({
onSuccess: () => {
// Optimistic updates and cache invalidation
utils.post.myPosts.invalidate();
},
});- Infinite Queries for pagination:
const { data, fetchNextPage, hasNextPage } = api.comment.getByPost.useInfiniteQuery(
{ postId: "123", limit: 10 },
{ getNextPageParam: (lastPage) => lastPage.nextCursor }
);- Optimistic Updates for real-time UX:
const toggleLike = api.like.togglePostLike.useMutation({
onMutate: async ({ postId }) => {
// Cancel outgoing refetches
await utils.like.getPostLikes.cancel({ postId });
// Snapshot previous value
const previousLikes = utils.like.getPostLikes.getData({ postId });
// Optimistically update
utils.like.getPostLikes.setData({ postId }, (old) => ({
...old!,
isLiked: !old?.isLiked,
count: old!.count + (old?.isLiked ? -1 : 1),
}));
return { previousLikes };
},
});- Error Handling with custom error types:
if (!post) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Post not found",
});
}The project implements real-time-like experiences through:
- Optimistic Updates for immediate UI feedback
- Cache Invalidation strategies for data consistency
- Infinite Pagination for smooth content loading
- Background Refetching for fresh data
src/
βββ app/ # Next.js App Router pages
β βββ (auth)/ # Authentication routes
β βββ (landing)/ # Public landing page
β βββ (main)/ # Protected application routes
βββ components/ # Reusable UI components
βββ server/
β βββ api/ # tRPC routers and procedures
β βββ db/ # Database schema and connection
βββ lib/ # Utility functions and configurations
βββ trpc/ # tRPC client setup
βββ styles/ # Global styles and Tailwind config
- Node.js 18+
- PostgreSQL database
- npm/yarn/pnpm
- Clone the repository
git clone https://github.com/yourusername/writespace.git
cd writespace- Install dependencies
npm install- Set up environment variables
cp .env.example .envFill in your environment variables:
# Database
DATABASE_URL='postgresql://user:password@localhost:5432/writespace'
# App URL
NEXT_PUBLIC_APP_URL='http://localhost:3000'
# Email (for development, use MOCK_SEND_EMAIL=true)
MOCK_SEND_EMAIL=true
SMTP_HOST='smtp.gmail.com'
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER='[email protected]'
SMTP_PASSWORD='your-app-password'
# OAuth (optional)
DISCORD_CLIENT_ID='your-discord-client-id'
DISCORD_CLIENT_SECRET='your-discord-client-secret'
# Stripe (optional)
STRIPE_API_KEY='sk_test_...'
STRIPE_WEBHOOK_SECRET='whsec_...'
STRIPE_PRO_MONTHLY_PLAN_ID='price_...'- Set up the database
npm run db:push- Start the development server
npm run devVisit http://localhost:3000 to see the application!
// In a React component
const createPost = api.post.create.useMutation({
onSuccess: () => {
router.push('/dashboard');
},
});
const handleSubmit = (data: CreatePostInput) => {
createPost.mutate(data);
};const { data: likes } = api.like.getPostLikes.useQuery({ postId });
const toggleLike = api.like.togglePostLike.useMutation({
// Optimistic updates for instant feedback
});const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = api.comment.getByPost.useInfiniteQuery(
{ postId, limit: 10 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);Run the test suite:
npm run test:e2eThe application is ready for deployment on platforms like Vercel, Netlify, or any Node.js hosting service.
npm run build
npm start- Set up a PostgreSQL database (Supabase, PlanetScale, etc.)
- Configure email service (SendGrid, AWS SES, etc.)
- Set up Stripe webhooks for production
- Update environment variables
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- SaasyKits for providing the excellent starter template that served as the foundation for this project
- T3 Stack for the excellent development experience and architectural patterns
- tRPC Team for the amazing type-safe API solution that makes full-stack TypeScript development seamless
- Next.js Team for the powerful React framework with App Router
- Tailwind CSS for the utility-first styling approach
- Lucia Auth team for the flexible authentication library
- Database Studio: Run
npm run db:studio
Abdallah Benassloun - Full-stack developer passionate about modern web technologies and type-safe development.
- π Specialized in Next.js, TypeScript, and tRPC
- π Building WriteSpace to showcase advanced full-stack patterns
- π Open to collaboration and learning opportunities
