LeadFlow CRM
×
Nhost Backend

Backend Nhost — Guía Técnica Completa

SaaS B2B de gestión de leads · PostgreSQL + Hasura GraphQL + Auth + Storage + Functions

Nhost SDK Hasura GraphQL PostgreSQL TypeScript React Serverless Functions Row-Level Security Real-time Subscriptions
🔐
Auth
Autenticación
Email/password, Google OAuth, magic link. JWT con claims de organización.
nhost.auth.*
🗄️
Database
PostgreSQL + GraphQL
4 tablas principales, relaciones, permisos por fila en Hasura. API GraphQL auto-generada.
nhost.graphql.*
📁
Storage
Adjuntos de Leads
Bucket "lead-attachments" con URLs firmadas. Control de acceso por organización.
nhost.storage.*
Functions
Webhook → Slack
Notificación automática a Slack cuando se crea un lead con score alto (≥80).
serverless
Flujo de Datos — LeadFlow CRM
React App
Frontend
@nhost/react
SDK Client
Hasura GraphQL
API Layer
PostgreSQL
Database
Functions
Webhooks/Events
Slack API
Notificaciones
1
Setup del Proyecto
✓ Local + Cloud
terminal
bash
# 1. Instalar Nhost CLI y dependencias npm install -g nhost npm install @nhost/nhost-js @nhost/react @nhost/react-apollo # 2. Inicializar proyecto LeadFlow nhost init leadflow-crm cd leadflow-crm # 3. Entorno local con Docker (PostgreSQL + Hasura + Auth + Storage) nhost up # Dashboard: http://localhost:1337 # GraphQL: http://localhost:1337/v1/graphql # Auth: http://localhost:1337/v1/auth # Storage: http://localhost:1337/v1/storage
src/lib/nhost.ts
TypeScript
import { NhostClient } from "@nhost/nhost-js"; export const nhost = new NhostClient({ subdomain: process.env.NEXT_PUBLIC_NHOST_SUBDOMAIN!, region: process.env.NEXT_PUBLIC_NHOST_REGION!, // dev local: subdomain: "localhost", region: "" }); // .env.local // NEXT_PUBLIC_NHOST_SUBDOMAIN=leadflow-crm // NEXT_PUBLIC_NHOST_REGION=eu-central-1
2
Schema PostgreSQL — 4 Tablas Principales
🎯 leads
ColumnaTipoNote
iduuidPK · default
org_iduuidFK → organizations
nametextnot null
emailtextunique / org
phonetextnullable
sourcetextweb/ads/referral
statustextnew/contacted/qualified
scoreinteger0-100
assigned_touuidFK → users
created_attimestamptznow()
💰 deals
ColumnaTipoNote
iduuidPK · default
org_iduuidFK → organizations
lead_iduuidFK → leads
titletextnot null
stagetextprospect/proposal/closed
valuenumericEUR
probabilityinteger0-100 %
closed_atdatenullable
created_attimestamptznow()
🏢 organizations
ColumnaTipoNote
iduuidPK · default
nametextnot null
plantextfree/pro/enterprise
max_leadsintegerplan limit
created_attimestamptznow()
📋 activities
ColumnaTipoNote
iduuidPK · default
org_iduuidFK → organizations
lead_iduuidFK → leads
typetextcall/email/meeting/note
notestextnullable
completedbooleandefault false
created_attimestamptznow()
nhost/migrations/default/1718000000000_init_leadflow/up.sql
SQL
CREATE TABLE organizations ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, plan text NOT NULL DEFAULT 'free', max_leads integer NOT NULL DEFAULT 100, created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE leads ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, name text NOT NULL, email text, phone text, source text NOT NULL DEFAULT 'web', status text NOT NULL DEFAULT 'new', score integer NOT NULL DEFAULT 0 CHECK (score BETWEEN 0 AND 100), assigned_to uuid REFERENCES auth.users(id), created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE deals ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, lead_id uuid NOT NULL REFERENCES leads(id) ON DELETE CASCADE, title text NOT NULL, stage text NOT NULL DEFAULT 'prospect', value numeric(10,2), probability integer DEFAULT 0 CHECK (probability BETWEEN 0 AND 100), closed_at date, created_at timestamptz NOT NULL DEFAULT now() );
3
Autenticación — Email + Google OAuth
src/hooks/useLeadFlowAuth.ts
TypeScript
import { useSignUpEmailPassword, useSignInEmailPassword, useSignOut, useAuthenticationStatus, useUserId } from "@nhost/react"; import { nhost } from "../lib/nhost"; export function useLeadFlowAuth() { const { signUpEmailPassword, isLoading: isSigningUp } = useSignUpEmailPassword(); const { signInEmailPassword, isLoading: isSigningIn } = useSignInEmailPassword(); const { signOut } = useSignOut(); const { isAuthenticated, isLoading } = useAuthenticationStatus(); const userId = useUserId(); const signUpWithOrg = async (email: string, password: string, orgName: string) => { return signUpEmailPassword(email, password, { displayName: orgName, metadata: { plan: "free", orgName }, }); }; const signInWithGoogle = () => { const { providerUrl } = nhost.auth.signIn({ provider: "google" }); window.location.href = providerUrl!; }; return { signUpWithOrg, signInEmailPassword, signInWithGoogle, signOut, isAuthenticated, isLoading, userId }; }
4
GraphQL — Queries, Mutations y Suscripciones en Tiempo Real
src/graphql/leads.ts
GraphQL
// Listar leads de la organización const GET_LEADS = gql` query GetLeads($org_id: uuid!, $status: String) { leads( where: { org_id: { _eq: $org_id } status: { _eq: $status } } order_by: { score: desc, created_at: desc } ) { id name email source status score created_at assignee { displayName avatarUrl } deals_aggregate { aggregate { count } } } } `; // Crear nuevo lead const INSERT_LEAD = gql` mutation InsertLead( $org_id: uuid! $name: String! $email: String $source: String! $score: Int! ) { insert_leads_one(object: { org_id: $org_id name: $name email: $email source: $source score: $score }) { id name score } } `;
src/graphql/deals-realtime.ts
GraphQL
// Suscripción tiempo real — Kanban de deals const SUBSCRIBE_DEALS_BOARD = gql` subscription DealsBoard($org_id: uuid!) { deals( where: { org_id: { _eq: $org_id } } order_by: { created_at: desc } ) { id title stage value probability closed_at lead { name email score } } } `; // Hook React para Kanban en tiempo real export function useDealsBoard(orgId: string) { const { data, loading, error } = useSubscription(SUBSCRIBE_DEALS_BOARD, { variables: { org_id: orgId }, }); const board = data?.deals.reduce((acc, deal) => { (acc[deal.stage] ||= []).push(deal); return acc; }, {} as Record<string, Deal[]>); return { board, loading, error }; }
5
Hasura — Permisos por Organización (Row-Level Security)
Tabla
SELECT
INSERT
UPDATE
DELETE
leads
org_id match
set org_id
owner only
deals
org_id match
set org_id
org_id match
org admin
activities
org_id match
set org_id
creator only
organizations
own org
admin role
nhost/metadata/databases/default/tables/public_leads.yaml
YAML
table: name: leads schema: public select_permissions: - role: user permission: columns: [id, org_id, name, email, source, status, score, assigned_to, created_at] filter: org_id: { _eq: X-Hasura-Org-Id } # Custom claim en JWT insert_permissions: - role: user permission: columns: [name, email, phone, source, status, score, assigned_to] set: org_id: X-Hasura-Org-Id # Forzado desde JWT, no del cliente
6
Serverless Function — Webhook Slack para Leads de Alto Score
nhost/functions/notify-hot-lead.ts
TypeScript
import { Request, Response } from "express"; /** * Disparado por Hasura Event Trigger cuando se inserta un lead con score >= 80. * Endpoint: POST /v1/functions/notify-hot-lead */ export default async function handler(req: Request, res: Response) { if (req.method !== "POST") return res.status(405).json({ error: "Method not allowed" }); const { event } = req.body; const lead = event.data.new; // Solo notificar si el score es alto if (lead.score < 80) return res.json({ skipped: true }); const slackPayload = { text: `🔥 *Hot Lead Detectado* — Score: ${lead.score}/100`, blocks: [{ type: "section", text: { type: "mrkdwn", text: `*${lead.name}* (${lead.email})\n` + `📊 Score: *${lead.score}* · Fuente: ${lead.source}\n` + `👉 <https://app.leadflow.io/leads/${lead.id}|Ver en LeadFlow>` } }] }; await fetch(process.env.SLACK_WEBHOOK_URL!, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(slackPayload), }); res.json({ notified: true, leadId: lead.id }); }
7
Storage — Adjuntos de Leads (Contratos, Propuestas)
src/lib/storage.ts
TypeScript
import { nhost } from "./nhost"; /** Subir adjunto privado asociado a un lead */ export async function uploadLeadAttachment( file: File, leadId: string, orgId: string ): Promise<string> { const ext = file.name.split(".").pop(); const { fileMetadata, error } = await nhost.storage.upload({ file, bucketId: "lead-attachments", // Bucket privado por org name: `${orgId}/${leadId}/${Date.now()}.${ext}`, }); if (error) throw new Error(error.message); // URL firmada (válida 24 h) para descarga privada const { presignedUrl } = await nhost.storage.getPresignedUrl({ fileId: fileMetadata!.id, }); return presignedUrl!.url; } /** Listar adjuntos de un lead */ export async function getLeadAttachments(leadId: string, orgId: string) { return nhost.storage.listFiles({ bucketId: "lead-attachments", prefix: `${orgId}/${leadId}/`, }); }
Checklist de Implementación — LeadFlow CRM
Instalar Nhost CLI + SDK (@nhost/nhost-js, @nhost/react)
Tiempo estimado: 5 min
setup
Levantar entorno local con nhost up (Docker)
Dashboard en localhost:1337
setup
Crear migración SQL con las 4 tablas del schema
organizations, leads, deals, activities
database
Configurar permisos Hasura con Row-Level Security por org_id
Custom JWT claim X-Hasura-Org-Id en Auth
security
Implementar autenticación con email/password y Google OAuth
Hook useLeadFlowAuth con metadata de organización
auth
Queries GraphQL para leads, deals, activities con paginación
Suscripción tiempo real para tablero Kanban de deals
graphql
Bucket "lead-attachments" con URLs firmadas (TTL 24 h)
Subida y listado de adjuntos por lead/organización
storage
Serverless function + Hasura Event Trigger para notificar hot leads a Slack
Se activa automáticamente cuando score >= 80
functions
Deploy en Nhost Cloud (EU region) con CI/CD de migraciones
nhost deploy --remote · variables de entorno en dashboard
deploy
🚀
Time to Production
De cero a backend completo en < 1 día. Sin gestionar servidores, sin configurar PostgreSQL, sin montar auth desde cero.
🔒
Seguridad por Defecto
Row-Level Security vía JWT claims. Cada agencia solo accede a sus datos. Zero-trust a nivel de base de datos.
Real-time Gratis
GraphQL subscriptions sobre WebSocket incluidas. El Kanban de deals se actualiza instantáneamente para todos los miembros del equipo.