Finanarepublic is a full-stack web application built with Next.js, Supabase, and Stripe.
It serves as a content platform for financial news and articles, offering both free and premium content to users. The project features a robust user authentication system, a seamless subscription model for premium access, and a focus on user experience and retention through features like personalized content and saved articles.
![]() |
![]() |
---|---|
![]() |
![]() |
![]() |
![]() |
The frontend uses client components to handle user-specific logic, such as checking subscription status before rendering an article. This ensures a reactive and secure user experience.
// src/components/ContentViewer.tsx
"use client";
import { useEffect } from "react";
import { useNavigation } from "next/navigation";
import { useUserSession } from "@/hooks/useUserSession";
import { useSubscriptionState } from "@/hooks/useSubscriptionState";
import BodyRenderer from "@/components/BodyRenderer";
import DiscussionForum from "@/components/DiscussionForum";
const ContentViewer: React.FC<{ contentItem: Content }> = ({ contentItem }) => {
const { currentUser } = useUserSession();
const hasActiveSubscription = useSubscriptionState();
const navigation = useNavigation();
// Redirect non-subscribed users from premium content
useEffect(() => {
if (contentItem.requiresSubscription && !hasActiveSubscription) {
navigation.redirectTo("/subscribe");
}
}, [contentItem.requiresSubscription, hasActiveSubscription, navigation]);
if (contentItem.requiresSubscription && !hasActiveSubscription) {
return <p>Redirecting to our subscription page...</p>;
}
return (
<main>
<h1>{contentItem.title}</h1>
<BodyRenderer content={contentItem.body} />
{currentUser && <DiscussionForum contentId={contentItem.id} />}
</main>
);
};
export default ContentViewer;
The backend is built with API Routes (serverless functions). This example shows how an authenticated user can initiate a subscription checkout.
// src/app/api/initiate-payment/route.ts
import { NextApiRequest, NextApiResponse } from "next";
import { initializePaymentProvider } from "@/lib/payment-provider";
import { initializeUserDatabase } from "@/lib/user-database";
export async function handler(
request: NextApiRequest,
response: NextApiResponse
) {
const paymentProvider = initializePaymentProvider();
const userDatabase = initializeUserDatabase();
// 1. Authenticate the user
const authToken = request.headers.authorization?.split(" ")[1];
const { user: currentUser } = await userDatabase.users.get(authToken);
if (!currentUser) {
return response.status(401).json({ message: "User not authenticated" });
}
// 2. Create a payment session
const { planIdentifier } = request.body;
const checkoutLink = await paymentProvider.checkout.createSession({
paymentType: "subscription",
lineItems: [{ plan: planIdentifier, quantity: 1 }],
successUrl: `${process.env.APP_URL}/subscription/success`,
cancelUrl: `${process.env.APP_URL}/subscription`,
metadata: {
userId: currentUser.id, // Securely pass the user ID
},
});
return response.status(200).json({ checkoutUrl: checkoutLink.url });
}
The architecture is designed around the Jamstack principles, using Next.js for static site generation (SSG) and server-side rendering (SSR) to ensure fast page loads. The core strategy for user retention and UX includes:
useFontSizePreference
allow users to customize their reading experience. The useArticleReads
hook tracks reading progress, opening possibilities for personalized recommendations.useSavedArticlesOptimized
) and comment (CommentSection.tsx
) encourages users to create an account and return to the platform.is_premium
. This creates a value proposition for users to subscribe, converting casual readers into paying customers. The usePremiumStatus
hook seamlessly manages the UI for premium and non-premium users.Security is managed at multiple layers: the edge, the application, and the database.
Edge & Application Security (Middleware): Next.js Middleware is used to implement crucial security headers on all incoming requests. This provides a first line of defense against common web vulnerabilities.
// src/middleware.ts
import { NextResponse, NextRequest } from "next/server";
export function securityMiddleware(request: NextRequest) {
const response = NextResponse.next();
// Define a strict Content Security Policy
const cspDirectives = [
"default-src 'self'",
"script-src 'self' https://trusted-scripts.com",
"style-src 'self' https://trusted-styles.com",
"img-src 'self' data:",
"font-src 'self'",
"frame-ancestors 'none'", // Disallow embedding in iframes
].join("; ");
response.headers.set("Content-Security-Policy", cspDirectives);
// Add other security headers
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set(
"Referrer-Policy",
"strict-origin-when-cross-origin"
);
return response;
}
Authentication & Authorization: Handled by Supabase Auth, which uses JWTs (JSON Web Tokens) stored securely in cookies. All API requests that require authentication are validated against the Supabase backend.
Database Security: Supabase provides Row Level Security (RLS) on its Postgres database. This ensures that even if an API request is compromised, users can only access or modify data they are permitted to. For example, a user can only update their own profile.
The application communicates with the Stripe API through dedicated Next.js API routes:
/api/create-checkout-session
: Authenticates a user and creates a Stripe session to redirect them to a secure payment page./api/cancel-subscription
: Allows a user to cancel their active subscription.The /api/stripe-webhook
endpoint is a critical component that listens for events from Stripe. This allows the application to react to real-world payment events asynchronously.
checkout.session.completed
: When a user successfully subscribes, this event is triggered. The webhook verifies the event, extracts the user.id
from the session metadata, and updates the corresponding user record in the Supabase database to grant premium access (is_premium = true
).customer.subscription.deleted
: When a subscription is canceled or expires, this event is used to revoke premium access for the user.This webhook is secured by verifying the Stripe signature and uses the Supabase Service Role Key for admin-level database operations.
// src/app/api/payment-webhook/route.ts
import { initializePaymentProvider } from "@/lib/payment-provider";
import { initializeAdminDatabase } from "@/lib/user-database";
const paymentProvider = initializePaymentProvider();
const appDatabase = initializeAdminDatabase(); // Use admin rights
export async function handleWebhookEvent(request: Request) {
const webhookSignature = request.headers.get("Payment-Provider-Signature");
const rawBody = await request.text();
// 1. Verify the event originated from the payment provider
const webhookPayload = paymentProvider.webhooks.verifySignature(
rawBody,
webhookSignature,
process.env.WEBHOOK_SIGNING_SECRET
);
// 2. Handle the specific event
switch (webhookPayload.type) {
case "payment.successful":
const paymentDetails = webhookPayload.data;
const customerId = paymentDetails.metadata.userId;
// 3. Update user's subscription status in the database
if (customerId) {
await appDatabase.users.updateSubscription({
userId: customerId,
isSubscribed: true,
subscriptionId: paymentDetails.subscriptionId,
});
}
break;
case "subscription.cancelled":
// Handle cancellation logic...
break;
}
return new Response("Webhook processed", { status: 200 });
}
The backend uses a Supabase Postgres database. The schema is designed to support the application's features, with tables for users
, articles
, comments
, saved_articles
, etc.
Authentication is handled entirely by Supabase Auth.
HttpOnly
cookies by the Supabase SSR library, ensuring they cannot be accessed by client-side JavaScript.Authorization
header for API calls, and the middleware/API routes validate it.auth.users
table, and a trigger typically copies public-safe information to a public.users
table....where auth.uid() = user_id
).delete_user_account_function.sql
) that can be called from a protected endpoint. This function securely deletes a user's data from all relevant tables, including their auth entry.articles
table. Users can create comments
which are linked to their user_id
and an article_id
.Cookie management is abstracted away by the supabase-js
SSR library.
sb-access-token
and sb-refresh-token
. They are configured as HttpOnly
and Secure
in production, making them inaccessible to XSS attacks./api/csrf
route, suggesting a Double Submit Cookie pattern or similar mechanism is used to prevent Cross-Site Request Forgery attacks on form submissions.The project follows a minimalistic and content-first design philosophy.