The Boring Portfolio Problem
Let's be honest. Most developer portfolios look exactly the same: a clean minimalist template, a grid of generic projects, and a bulleted list of tech stacks.
When I built my portfolio, I wanted to showcase real-world production engineering. Instead of pushing 10 tiny code sandboxes, I decided to focus on deep-dive case studies.
This is the breakdown of Äbasto, a full-stack, multi-tenant B2B SaaS for warehouse and POS management that handles data isolation, dynamic subdomains, and an automated grace-period subscription model under a single pnpm workspace monorepo.
🏗️ The Architecture Stack
he project is structured as a scalable monorepo using pnpm workspaces:
Frontend: Next.js 16 App Router, Zustand (state persistence), and Tailwind CSS v4.
Backend: NestJS 11, TypeORM, and PostgreSQL.
🛡️ Challenge 1: Tenant Data Isolation at DB Level (PostgreSQL RLS)
When building a B2B SaaS where multiple independent warehouses manage inventory, global multi-tenancy leaks are your worst nightmare. Adding WHERE warehouse_id = X to every database query is prone to human error and scale bugs.
The Solution: Row-Level Security (RLS)
I delegated data isolation directly to PostgreSQL using Row-Level Security.
Every database transaction runs securely isolated. A custom JwtAuthGuard in NestJS intercepts the request, decodes the tenant data, and injects session variables directly using SQL SET LOCAL commands:
// A high-level view of injecting session context dynamically
async function injectTenantContext(queryRunner: QueryRunner, warehouseId: string) {
// Safe runtime execution within the request transaction block
await queryRunner.query(`SET LOCAL app.current_warehouse_id = '${warehouseId}'`);
}
In the DB layer, tables enforce isolation natively:
ALTER TABLE inventory ENABLE ROW LEVEL SECURITY;
CREATE POLICY warehouse_isolation_policy ON inventory
USING (warehouse_id = NULLIF(current_setting('app.current_warehouse_id', true), ''));
This means even if a developer forgets to filter by warehouse in a frontend component, PostgreSQL will completely block cross-tenant data leaks.
🌐 Challenge 2: Dynamic Multi-Tenant Subdomains in Next.js 16
wanted every warehouse owner to have their own distinct subdomain (e.g., my-store.lvh.me:3000).
The Solution: Dynamic Rewrite Proxy
Instead of cluttering the system with a heavy middleware.ts, I utilized a specialized server-side proxy.ts execution block in Next.js 16. It dynamically reads the Host header and rewrites paths directly:
export function handleSubdomainRewrite(requestHeaders: Headers) {
const host = requestHeaders.get('host'); // e.g., 'bodega-x.lvh.me:3000'
const subdomain = host.split('.')[0];
// Bypass reserved internal system paths natively
if (subdomain === 'admin' || subdomain === 'www') {
return null;
}
// Perform an internal server rewrite to the dynamic store template
return `/store/${subdomain}`;
}
The Catch: Server-Side Token Security
o prevent identity spoofing, the proxy reads the token cookie (configured with a root domain scope domain=.lvh.me), decodes the JWT payload on the server side, and natively verifies if the token's authorized warehouse matches the requested subdomain. If there is a mismatch, it triggers an immediate rewrite redirect to /no-access.
⏳ Challenge 3: Automated Lock-Out & Subscription Engine
A true SaaS needs to handle monetization and enforcement without blocking access to historical data arbitrarily. I designed a customized multi-state subscription model with an embedded 3-day grace period.
Active State -> [Expiration Date] -> 3-Day Grace Period (Banners) -> Fully Locked POS Screen
The Backend Enforcement Guard
We built a centralized SubscriptionGuard applied globally to all mutable endpoints (POST, PATCH, DELETE) across the products, inventory, and supplier components:
@Injectable()
export class SubscriptionGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const { expiresAt, gracePeriodDays } = req.user; // Appended by auth verification
const absoluteDeadline = new Date(expiresAt);
absoluteDeadline.setDate(absoluteDeadline.getDate() + (gracePeriodDays || 3));
if (new Date() > absoluteDeadline) {
throw new ForbiddenException('Subscription completely expired. Write operations locked.');
}
return true; // GET endpoints remain open cleanly
}
}
The Frontend Reaction Flow
Within 5 days of expiration: A contextual Amber
SubscriptionBannerpops up in the Dashboard.During Grace Period: An Orange warning stays fixed.
Past Grace Period: A full-screen
SubscriptionLockoverlay takes over the POS component with a pre-configured WhatsApp manual link (wa.me) leveraging a centralized support module to handle instant payment updates.
📨 Challenge 4: Transactional Communications via Resend & Manual WhatsApp Fallbacks
To ensure smooth operational communication without overhead costs, I implemented a hybrid Dual-Channel Notification System:
1. Channel A (Automated Transacational Mail): A global NotificationsModule hooks into backend services using the Resend SDK. It triggers beautifully designed, dark Neobrutalist HTML templates on critical events:
sendWelcomeEmail: Sends temporary credentials and system subdomain links immediately upon setup.sendSubscriptionExpiringEmail: Triggered daily at noon via a NestJS @nestjs/schedule CRON job with clean in-memory de-duplication (Set).
- Channel B (Manual WhatsApp Flows): For payment tracking, the SuperAdmin dashboard incorporates manual communication helpers that parse tenant states dynamically into contextual text reminders, generating a frictionless single-click chat initiation link.
🧠 Key Takeaways
Building Äbasto proved that your portfolio doesn't need to be an archive of 20 unmaintained projects. Dedicating your space to full-scale engineering breakdowns showcases:
Deep comprehension of DB performance and security models.
Familiarity with server-side network engineering architecture (proxies, domain parsing).
Product mindset implementation (subscription gates, user retention design).
What does your current portfolio project stack look like? Are you team Single-DB isolation or separated clusters? Let's discuss in the comments below!
Top comments (0)