<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alejandro Olivar</title>
    <description>The latest articles on DEV Community by Alejandro Olivar (@aomalejandrodev).</description>
    <link>https://dev.to/aomalejandrodev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4000724%2Ffb0c1a77-f1d2-4b3d-bf34-1d3dae5e1ab5.png</url>
      <title>DEV Community: Alejandro Olivar</title>
      <link>https://dev.to/aomalejandrodev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aomalejandrodev"/>
    <language>en</language>
    <item>
      <title>(Spanish)📝 Caso de Estudio: Si Presto! — De cuadernos de notas a una arquitectura Offline-First en Flutter</title>
      <dc:creator>Alejandro Olivar</dc:creator>
      <pubDate>Sun, 12 Jul 2026 17:04:34 +0000</pubDate>
      <link>https://dev.to/aomalejandrodev/caso-de-estudio-si-presto-de-cuadernos-de-notas-a-una-arquitectura-offline-first-en-flutter-32bb</link>
      <guid>https://dev.to/aomalejandrodev/caso-de-estudio-si-presto-de-cuadernos-de-notas-a-una-arquitectura-offline-first-en-flutter-32bb</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;📌 Introducción: El origen del problema (De una libreta a código)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;El software más útil nace de resolver problemas reales cotidianos. Este proyecto comenzó cuando observé a un familiar que prestaba dinero de manera informal. Su sistema de gestión consistía en una libreta física y hojas de cálculo en Excel.&lt;/p&gt;

&lt;p&gt;El resultado era caótico:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Olvidos frecuentes en las fechas de cobro.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Falta de claridad al calcular abonos parciales o intereses acumulados.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Horas perdidas redactando manualmente estados de cuenta por WhatsApp.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para solucionar esto, decidí diseñar e implementar Si Presto!, una aplicación móvil nativa construida con Flutter y Firebase enfocada en el control de préstamos informales, optimizada para contextos de baja conectividad y con un modelo híbrido de monetización.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;🏗️ La Arquitectura: Diseño de Software y Flujo de Datos&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Para asegurar un rendimiento óptimo en dispositivos de cualquier gama, estructuré la aplicación bajo una Arquitectura Limpia (Clean Architecture) organizada en un Monorepo móvil con separación estricta de responsabilidades:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
  &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;core&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;                  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Temas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PDF&lt;/span&gt; &lt;span class="nx"&gt;engines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notificaciones&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;
  &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;                &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Entidades&lt;/span&gt; &lt;span class="nf"&gt;inmutables &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Freezed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="nx"&gt;interfaces&lt;/span&gt;
  &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;                  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Fuentes&lt;/span&gt; &lt;span class="nx"&gt;de&lt;/span&gt; &lt;span class="nf"&gt;datos &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Isar&lt;/span&gt; &lt;span class="nx"&gt;NoSQL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Firebase&lt;/span&gt; &lt;span class="nx"&gt;Auth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="nx"&gt;modelos&lt;/span&gt;
  &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;presentation&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;          &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Gestión&lt;/span&gt; &lt;span class="nx"&gt;de&lt;/span&gt; &lt;span class="nf"&gt;estado &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Riverpod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="nx"&gt;páginas&lt;/span&gt; &lt;span class="nx"&gt;UI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;El Flujo de Datos Híbrido:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;UI ➔ Riverpod StateNotifiers ➔ Repositorios ➔ Isar DB (Local) ➔ Firebase Cloud Backup (Premium).&lt;/p&gt;

&lt;p&gt;Toda la generación de código inmutable y reactivo se automatizó utilizando &lt;code&gt;build_runner&lt;/code&gt; con herramientas como &lt;code&gt;@freezed&lt;/code&gt;, &lt;code&gt;@riverpod&lt;/code&gt; e &lt;code&gt;@Isar&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;🛡️ Los 4 Grandes Retos Técnicos de Ingeniería&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Sincronización Offline-First y Consistencia de Datos en Venezuela&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El Problema:&lt;/strong&gt; La aplicación debe funcionar sin fricciones en zonas con conectividad intermitente o nula. Si el cobrador registra un abono en la calle sin señal, los saldos locales no pueden duplicarse ni corromperse al recuperar la red.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La Solución:&lt;/strong&gt; Implementé un modelo Offline-First utilizando Isar Database como almacenamiento NoSQL local de alto rendimiento. La aplicación realiza todas las operaciones CRUD de forma síncrona en la base de datos local y, mediante un servicio de sincronización en segundo plano, respalda la información en la nube con Cloud Firestore únicamente si el usuario cuenta con conectividad y posee el plan Premium.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Generación Dinámica de PDF Nativos en Dispositivos de Gama Baja&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El Problema:&lt;/strong&gt; Generar reportes financieros detallados consume memoria. Si el renderizado se realiza de forma ineficiente, los teléfonos de gama baja pueden sufrir cierres inesperados (crashes por OOM).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La Solución:&lt;/strong&gt; Diseñé un motor de renderizado transaccional utilizando los paquetes nativos &lt;code&gt;pdf&lt;/code&gt; y &lt;code&gt;printing&lt;/code&gt;. El motor procesa las colecciones inmutables de transacciones en bloques (chunks) directamente en la memoria caché del dispositivo, ensamblando el PDF en tiempo real para que el cobrador pueda exportarlo y compartirlo inmediatamente por WhatsApp sin sobrecargar el procesador del teléfono.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Modelo de Monetización Híbrido (AdMob + RevenueCat)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El Problema:&lt;/strong&gt; Mantener la aplicación requiere rentabilidad, pero saturar una herramienta financiera de cobros con anuncios intrusivos arruina por completo la experiencia de usuario (UX).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La Solución:&lt;/strong&gt; Implementé un esquema Freemium controlado minuciosamente desde la capa de datos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Free Tier: Límites lógicos quemados en el repositorio (Máximo 3 deudores y 4 pagos por préstamo) financiados mediante banners publicitarios no intrusivos usando Google Mobile Ads.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Premium Tier: Integrado de manera limpia con el SDK de RevenueCat. Al detectar la compra activa mediante el singleton &lt;code&gt;PurchaseService&lt;/code&gt;, la aplicación remueve por completo los anuncios, desbloquea deudores/pagos ilimitados, habilita la exportación a PDF y activa el respaldo en la nube con Cloud Firestore.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Automatización de Alertas y Notificaciones Locales&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Para eliminar la dependencia constante de servidores externos (Push Notifications) que pueden fallar si el dispositivo no tiene datos móviles, configuré un servicio autónomo basado en &lt;code&gt;flutter_local_notifications&lt;/code&gt;. La aplicación programa internamente recordatorios de cobro locales basados en las fechas límite de las entidades del dominio de datos, manteniendo al cobrador alerta sin consumir datos de red.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;🎨 Identidad Visual: Estética Neobrutalista Oscura&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Para mantener la coherencia con mi marca personal, la interfaz de Si Presto! adopta un estilo Neobrutalista Oscuro caracterizado por:&lt;/p&gt;

&lt;p&gt;Un fondo oscuro profundo de alto contraste combinado con un verde neón eléctrico (&lt;code&gt;#00FF99&lt;/code&gt;) para destacar los saldos positivos.&lt;/p&gt;

&lt;p&gt;Tarjetas de resumen financiero con bordes marcados y sombras sólidas planas desplazadas, rompiendo con los diseños tradicionales y planos de las aplicaciones bancarias comunes.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;🧠 Lecciones de Ingeniería de Producto&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Construir &lt;strong&gt;Si Presto&lt;/strong&gt;! partiendo de una necesidad familiar real me enseñó que la arquitectura de software senior no se mide por la cantidad de servicios en la nube que utilices, sino por saber delegar eficientemente el cómputo. Al elegir almacenar datos localmente con Isar, renderizar PDFs nativos en el cliente y programar notificaciones locales, reduje los costes de infraestructura de la aplicación a prácticamente cero dólares para el nivel gratuito, asegurando un producto escalable y altamente rentable.&lt;/p&gt;

&lt;p&gt;Link: &lt;a href="https://portfolio-three-topaz-81.vercel.app/es/projects/si-presto" rel="noopener noreferrer"&gt;https://portfolio-three-topaz-81.vercel.app/es/projects/si-presto&lt;/a&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>flutter</category>
      <category>portfolio</category>
    </item>
    <item>
      <title>📝 Caso de Estudio: Cómo construir un SaaS Multi-tenant con Next.js 16, NestJS 11 y PostgreSQL RLS</title>
      <dc:creator>Alejandro Olivar</dc:creator>
      <pubDate>Sat, 27 Jun 2026 16:05:22 +0000</pubDate>
      <link>https://dev.to/aomalejandrodev/caso-de-estudio-como-construir-un-saas-multi-tenant-con-nextjs-16-nestjs-11-y-postgresql-rls-25hk</link>
      <guid>https://dev.to/aomalejandrodev/caso-de-estudio-como-construir-un-saas-multi-tenant-con-nextjs-16-nestjs-11-y-postgresql-rls-25hk</guid>
      <description>&lt;h2&gt;
  
  
  Introducción: El problema de los portafolios aburridos
&lt;/h2&gt;

&lt;p&gt;Seamos honestos. La mayoría de los portafolios de desarrolladores se ven exactamente iguales: una plantilla minimalista genérica, una cuadrícula con proyectos simples y una lista de tecnologías que nadie entra a auditar.&lt;/p&gt;

&lt;p&gt;Cuando rediseñé mi espacio web, quise enfocarlo de otra manera: mostrar ingeniería real aplicada a producción. En lugar de subir 10 proyectos pequeños, decidí desglosar casos de estudio profundos.&lt;/p&gt;

&lt;p&gt;Este es el análisis técnico de Äbasto, un SaaS B2B full-stack para la gestión de inventario y puntos de venta (POS) que resuelve el aislamiento de datos, subdominios dinámicos y un sistema de suscripción con periodos de gracia bajo un único monorepo con &lt;code&gt;pnpm workspaces&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏗️ El Stack Tecnológico
&lt;/h2&gt;

&lt;p&gt;El proyecto está estructurado de manera eficiente en un monorepo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt;: Next.js 16 (App Router), Zustand (para persistencia de estado local) y Tailwind CSS v4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend&lt;/strong&gt;: NestJS 11, TypeORM y PostgreSQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛡️ Reto 1: Aislamiento de datos a nivel de Base de Datos (PostgreSQL RLS)
&lt;/h2&gt;

&lt;p&gt;Cuando construyes un SaaS donde múltiples bodegas independientes gestionan inventarios, las filtraciones de datos entre clientes son tu peor pesadilla. Depender de añadir un &lt;code&gt;WHERE warehouse_id = X&lt;/code&gt; en cada query del backend es peligroso y propenso a errores humanos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La Solución: Row-Level Security (RLS)&lt;/strong&gt;&lt;br&gt;
Delegué el aislamiento de datos directamente al motor de PostgreSQL usando seguridad a nivel de fila.&lt;/p&gt;

&lt;p&gt;Cada transacción se ejecuta de forma aislada. Un JwtAuthGuard personalizado en NestJS intercepta la petición, decodifica los datos del inquilino (tenant) e inyecta variables de sesión en la base de datos usando comandos &lt;code&gt;SQL SET LOCAL&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Inyección dinámica del contexto del inquilino&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;injectTenantContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QueryRunner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;warehouseId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Ejecución segura dentro del bloque de la transacción de la petición&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SET LOCAL app.current_warehouse_id = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;warehouseId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En la base de datos, las tablas imponen el aislamiento de forma nativa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ALTER&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt; &lt;span class="nx"&gt;ENABLE&lt;/span&gt; &lt;span class="nx"&gt;ROW&lt;/span&gt; &lt;span class="nx"&gt;LEVEL&lt;/span&gt; &lt;span class="nx"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;CREATE&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt; &lt;span class="nx"&gt;warehouse_isolation_policy&lt;/span&gt; &lt;span class="nx"&gt;ON&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt;
    &lt;span class="nc"&gt;USING &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;warehouse_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app.current_warehouse_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto significa que incluso si a un desarrollador se le olvida filtrar por bodega en el código, &lt;strong&gt;PostgreSQL bloqueará por defecto cualquier intento de acceso cruzado&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌐 Reto 2: Subdominios dinámicos multi-tenant en Next.js 16
&lt;/h2&gt;

&lt;p&gt;Quería que cada dueño de bodega tuviera un subdominio único y limpio (ej: &lt;code&gt;mi-tienda.lvh.me:3000&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La Solución: Proxy de reescritura dinámica&lt;/strong&gt;&lt;br&gt;
En lugar de saturar el sistema con un &lt;code&gt;middleware.ts&lt;/code&gt; pesado, utilicé un bloque de ejecución &lt;code&gt;proxy.ts&lt;/code&gt; en el servidor de Next.js 16. Este lee la cabecera &lt;code&gt;Host&lt;/code&gt; en tiempo real y reescribe las rutas internamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// frontend/src/proxy.ts (Lógica simplificada)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSubdomainRewrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ej: 'bodega-x.lvh.me:3000'&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subdomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="c1"&gt;// Ignorar las rutas del sistema reservadas internamente&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subdomain&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;subdomain&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;www&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Realiza una reescritura interna hacia la ruta dinámica de la tienda&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`/store/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subdomain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;El truco de seguridad Server-Side&lt;/strong&gt;&lt;br&gt;
Para evitar que un usuario manipule la URL y suplante la identidad de otra tienda, el proxy lee la cookie de sesión &lt;code&gt;token&lt;/code&gt; (configurada con alcance global &lt;code&gt;domain=.lvh.me&lt;/code&gt;), decodifica el JWT en el servidor y verifica si la bodega autorizada coincide estrictamente con el subdominio solicitado. Si no hay coincidencia, redirige inmediatamente a &lt;code&gt;/no-access&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  ⏳ Reto 3: Restricción automatizada y motor de suscripciones
&lt;/h2&gt;

&lt;p&gt;Un SaaS real necesita monetizarse y aplicar restricciones de uso de forma inteligente, sin bloquear el acceso histórico del cliente arbitrariamente. Diseñé un modelo con un &lt;strong&gt;periodo de gracia de 3 días&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Estado Activo -&amp;gt; [Fecha Expiración] -&amp;gt; Periodo de Gracia (Banners) -&amp;gt; Bloqueo Completo de Operaciones
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El Guardián del Backend (SubscriptionGuard)&lt;br&gt;
Construí un &lt;code&gt;SubscriptionGuard&lt;/code&gt; global aplicado a todos los endpoints de escritura (&lt;code&gt;POST, PATCH, DELETE&lt;/code&gt;) en los módulos críticos de productos, inventario y proveedores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SubscriptionGuard&lt;/span&gt; &lt;span class="kr"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;CanActivate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;canActivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;switchToHttp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getRequest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gracePeriodDays&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Datos inyectados en la sesión&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;absoluteDeadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;absoluteDeadline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;absoluteDeadline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gracePeriodDays&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;absoluteDeadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ForbiddenException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Suscripción expirada. Operaciones de escritura bloqueadas.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Las peticiones GET (lectura) siguen abiertas&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comportamiento en el Frontend&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Faltando 5 días para vencer&lt;/strong&gt;: Un banner ámbar dinámico (&lt;code&gt;SubscriptionBanner&lt;/code&gt;) aparece en el Dashboard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Durante el Periodo de Gracia&lt;/strong&gt;: El banner cambia a color naranja de advertencia.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pasado el Periodo de Gracia&lt;/strong&gt;: Un componente de bloqueo de pantalla completa (&lt;code&gt;SubscriptionLock&lt;/code&gt;) toma el control del punto de venta (POS) mostrando un botón directo de soporte mediante enlaces dinámicos de WhatsApp (&lt;code&gt;wa.me&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📨 Reto 4: Notificaciones híbridas (Emails con Resend y atajos de WhatsApp)
&lt;/h2&gt;

&lt;p&gt;Para mantener una comunicación operativa fluida sin incurrir en costes elevados de infraestructura, implementé un &lt;strong&gt;sistema de notificación de doble canal&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Canal A (Mails transaccionales automatizados)&lt;/strong&gt;: Un módulo global de notificaciones se conecta con el &lt;strong&gt;SDK de Resend&lt;/strong&gt; en el backend. Dispara correos formateados con plantillas HTML de estilo neobrutalista oscuro ante eventos críticos (bienvenida, restablecimiento de contraseña y alertas de facturación gestionadas por un CRON job diario a mediodía).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Canal B (Flujos manuales de WhatsApp)&lt;/strong&gt;: Desde el panel de administración, el SuperAdmin cuenta con botones contextuales que generan mensajes dinámicos basados en el estado de cuenta del cliente, facilitando el cobro manual con un solo clic.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🧠 Conclusiones
&lt;/h2&gt;

&lt;p&gt;Desarrollar &lt;strong&gt;Äbasto&lt;/strong&gt; me demostró que vale mucho más la pena documentar la resolución de problemas arquitectónicos reales y complejos que acumular repositorios sencillos. Esto valida habilidades críticas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Comprensión avanzada de seguridad y rendimiento en bases de datos.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Arquitectura de redes y enrutamiento en el servidor (proxies, manejo de DNS internos).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enfoque de producto y lógica de negocio orientada a la monetización.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Link: &lt;a href="https://portfolio-three-topaz-81.vercel.app/en/projects/abasto" rel="noopener noreferrer"&gt;https://portfolio-three-topaz-81.vercel.app/en/projects/abasto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;¿Cómo gestionas tú el aislamiento de datos en tus proyectos SaaS? ¿Prefieres bases de datos separadas o aislamiento lógico por fila (RLS)? ¡Debatamos en los comentarios!&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>nextjs</category>
      <category>nestjs</category>
      <category>node</category>
    </item>
    <item>
      <title>Deep Dive: Handling Multi-Tenancy Subdomains and DB Isolation without breaking Next.js 16</title>
      <dc:creator>Alejandro Olivar</dc:creator>
      <pubDate>Thu, 25 Jun 2026 16:07:04 +0000</pubDate>
      <link>https://dev.to/aomalejandrodev/deep-dive-handling-multi-tenancy-subdomains-and-db-isolation-without-breaking-nextjs-16-277b</link>
      <guid>https://dev.to/aomalejandrodev/deep-dive-handling-multi-tenancy-subdomains-and-db-isolation-without-breaking-nextjs-16-277b</guid>
      <description>&lt;h2&gt;
  
  
  The Boring Portfolio Problem
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏗️ The Architecture Stack
&lt;/h2&gt;

&lt;p&gt;he project is structured as a scalable monorepo using pnpm workspaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt;: Next.js 16 App Router, Zustand (state persistence), and Tailwind CSS v4.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Backend&lt;/strong&gt;: NestJS 11, TypeORM, and PostgreSQL.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🛡️ Challenge 1: Tenant Data Isolation at DB Level (PostgreSQL RLS)
&lt;/h2&gt;

&lt;p&gt;When building a B2B SaaS where multiple independent warehouses manage inventory, global multi-tenancy leaks are your worst nightmare. Adding &lt;code&gt;WHERE warehouse_id = X&lt;/code&gt; to every database query is prone to human error and scale bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution: Row-Level Security (RLS)&lt;/strong&gt;&lt;br&gt;
I delegated data isolation directly to PostgreSQL using Row-Level Security.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A high-level view of injecting session context dynamically&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;injectTenantContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QueryRunner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;warehouseId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Safe runtime execution within the request transaction block&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SET LOCAL app.current_warehouse_id = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;warehouseId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the DB layer, tables enforce isolation natively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ALTER&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt; &lt;span class="nx"&gt;ENABLE&lt;/span&gt; &lt;span class="nx"&gt;ROW&lt;/span&gt; &lt;span class="nx"&gt;LEVEL&lt;/span&gt; &lt;span class="nx"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;CREATE&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt; &lt;span class="nx"&gt;warehouse_isolation_policy&lt;/span&gt; &lt;span class="nx"&gt;ON&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt;
    &lt;span class="nc"&gt;USING &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;warehouse_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app.current_warehouse_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means even if a developer forgets to filter by warehouse in a frontend component, PostgreSQL will completely block cross-tenant data leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌐 Challenge 2: Dynamic Multi-Tenant Subdomains in Next.js 16
&lt;/h2&gt;

&lt;p&gt;wanted every warehouse owner to have their own distinct subdomain (e.g., &lt;code&gt;my-store.lvh.me:3000&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution: Dynamic Rewrite Proxy&lt;/strong&gt;&lt;br&gt;
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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSubdomainRewrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// e.g., 'bodega-x.lvh.me:3000'&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subdomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="c1"&gt;// Bypass reserved internal system paths natively&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subdomain&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;subdomain&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;www&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Perform an internal server rewrite to the dynamic store template&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`/store/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subdomain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Catch: Server-Side Token Security&lt;/strong&gt;&lt;br&gt;
o prevent identity spoofing, the proxy reads the &lt;code&gt;token&lt;/code&gt; cookie (configured with a root domain scope &lt;code&gt;domain=.lvh.me&lt;/code&gt;), 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 &lt;code&gt;/no-access&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  ⏳ Challenge 3: Automated Lock-Out &amp;amp; Subscription Engine
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Active State -&amp;gt; [Expiration Date] -&amp;gt; 3-Day Grace Period (Banners) -&amp;gt; Fully Locked POS Screen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Backend Enforcement Guard&lt;/strong&gt;&lt;br&gt;
We built a centralized &lt;code&gt;SubscriptionGuard&lt;/code&gt; applied globally to all mutable endpoints (&lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;) across the products, inventory, and supplier components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SubscriptionGuard&lt;/span&gt; &lt;span class="kr"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;CanActivate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;canActivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;switchToHttp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getRequest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gracePeriodDays&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Appended by auth verification&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;absoluteDeadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;absoluteDeadline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;absoluteDeadline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gracePeriodDays&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;absoluteDeadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ForbiddenException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Subscription completely expired. Write operations locked.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// GET endpoints remain open cleanly&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Frontend Reaction Flow&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Within 5 days of expiration:&lt;/strong&gt; A contextual Amber &lt;code&gt;SubscriptionBanner&lt;/code&gt; pops up in the Dashboard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;During Grace Period:&lt;/strong&gt; An Orange warning stays fixed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Past Grace Period:&lt;/strong&gt; A full-screen &lt;code&gt;SubscriptionLock&lt;/code&gt; overlay takes over the POS component with a pre-configured WhatsApp manual link (&lt;code&gt;wa.me&lt;/code&gt;) leveraging a centralized support module to handle instant payment updates.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📨 Challenge 4: Transactional Communications via Resend &amp;amp; Manual WhatsApp Fallbacks
&lt;/h2&gt;

&lt;p&gt;To ensure smooth operational communication without overhead costs, I implemented a hybrid Dual-Channel Notification System:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Channel A (Automated Transacational Mail):&lt;/strong&gt; A global &lt;code&gt;NotificationsModule&lt;/code&gt; hooks into backend services using the Resend SDK. It triggers beautifully designed, dark Neobrutalist HTML templates on critical events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;sendWelcomeEmail&lt;/code&gt;: Sends temporary credentials and system subdomain links immediately upon setup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;sendSubscriptionExpiringEmail&lt;/code&gt;: Triggered daily at noon via a NestJS @nestjs/schedule CRON job with clean in-memory de-duplication (Set).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Channel B (Manual WhatsApp Flows):&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🧠 Key Takeaways
&lt;/h2&gt;

&lt;p&gt;Building &lt;strong&gt;Äbasto&lt;/strong&gt; proved that your portfolio doesn't need to be an archive of 20 unmaintained projects. Dedicating your space to full-scale engineering breakdowns showcases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Deep comprehension of DB performance and security models.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Familiarity with server-side network engineering architecture (proxies, domain parsing).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Product mindset implementation (subscription gates, user retention design).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Link: &lt;a href="https://portfolio-three-topaz-81.vercel.app/en/projects/abasto" rel="noopener noreferrer"&gt;https://portfolio-three-topaz-81.vercel.app/en/projects/abasto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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!&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>nestjs</category>
      <category>postgres</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
