Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
YouTube Security VideosOpenAI: “It Blew Me Away” | Box’s First Look at GPT-6 Astra(21.09.2026 um 18:13 Uhr)
YouTube Security VideosGoogle Ads: Win the holiday season in 90 seconds | Rethink Retail 2026(21.09.2026 um 18:13 Uhr)
YouTube Security VideosPC-WELT: 5 crazy Sitzpositionen, die jeder Gamer kennt 😀(21.09.2026 um 18:52 Uhr)
Videos & KonferenzenMariaDB Foundation: The Fork Series #1 - The Future of Databases(21.09.2026 um 19:18 Uhr)
Videos & KonferenzenPC-WELT: 5 crazy Sitzpositionen, die jeder Gamer kennt 😀(21.09.2026 um 18:52 Uhr)
Unix & Linux ServerSecurity: Denial of Service in Memcached (Ubuntu)(21.09.2026 um 19:21 Uhr)
YouTube Security VideosOpenAI: “It Blew Me Away” | Box’s First Look at GPT-6 Astra(21.09.2026 um 18:13 Uhr)
YouTube Security VideosGoogle Ads: Win the holiday season in 90 seconds | Rethink Retail 2026(21.09.2026 um 18:13 Uhr)
YouTube Security VideosPC-WELT: 5 crazy Sitzpositionen, die jeder Gamer kennt 😀(21.09.2026 um 18:52 Uhr)
Videos & KonferenzenMariaDB Foundation: The Fork Series #1 - The Future of Databases(21.09.2026 um 19:18 Uhr)
Videos & KonferenzenPC-WELT: 5 crazy Sitzpositionen, die jeder Gamer kennt 😀(21.09.2026 um 18:52 Uhr)
Unix & Linux ServerSecurity: Denial of Service in Memcached (Ubuntu)(21.09.2026 um 19:21 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3

Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 Hace unos meses decidí resolver un problema que tenía mi propio consorcio: llevar el control de expensas en planillas de Excel, con pagos en efe…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!




Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3



Hace unos meses decidí resolver un problema que tenía mi propio consorcio: llevar el control de expensas en planillas de Excel, con pagos en efectivo sin recibo, y sin forma de saber quién debía qué. Lo que empezó como un proyecto de fin de semana terminó siendo Cuotia, una plataforma completa que hoy usan consorcios, escuelas, gimnasios y clubes en Argentina.



En este artículo cuento cómo está construida, qué decisiones técnicas tomé y qué aprendí en el camino.






Stack elegido



Backend: FastAPI (Python) + SQLAlchemy async + PostgreSQL 16

Frontend: Vue 3 + Vite + TailwindCSS + Pinia

Deploy: Docker Compose + Nginx en VPS Contabo

Pagos: MercadoPago API + Resend (emails)



No usé Next.js ni Nuxt porque quería separación clara entre API y frontend. FastAPI me da validación automática con Pydantic, docs en /docs sin esfuerzo y async nativo que necesitaba para las consultas concurrentes.





Arquitectura general



Internet → Cloudflare → Nginx → {

/api/* → FastAPI (puerto 8000)

/* → Vue SPA (Nginx interno)

}



Cuatro contenedores Docker:





  • db — PostgreSQL


  • api — FastAPI con uvicorn


  • frontend — Nginx sirviendo el build de Vite


  • nginx — reverse proxy + SSL con Certbot



El docker-compose de producción tiene todo en una red interna (app-network) y solo expone los puertos 80/443 al exterior.





El desafío del multi-rubro



El feature más interesante técnicamente fue hacer que la misma plataforma sirva para rubros completamente distintos sin duplicar código.



Un consorcio habla de departamentos, expensas y propietarios. Una escuela habla de alumnos, aranceles y grados. Un gimnasio habla de socios, mensualidades y planes.



La solución: una tabla rubros en la DB con todos los labels configurables:



python

class Rubro(Base):

tablename = "rubros"

id = Column(Integer, primary_key=True)

codigo = Column(String(30), unique=True) # "consorcio", "escuela", "gimnasio"

nombre = Column(String(100))

label_unidad = Column(String(50)) # "Departamento" / "Alumno" / "Socio"

label_miembro = Column(String(50)) # "Inquilino" / "Padre/Tutor" / "Socio"

label_cuota = Column(String(50)) # "Expensa" / "Arancel" / "Mensualidad"

label_grupo = Column(String(50)) # "Piso" / "Grado" / "Turno"

tiene_piso = Column(Boolean)

tiene_coeficiente = Column(Boolean)



Y en el frontend, un Pinia store que carga el vocabulario del rubro del usuario:



typescript

// stores/vocabulario.ts

const v = computed(() => ({

unidad: vocab.value?.label_unidad ?? 'Unidad',

miembro: vocab.value?.label_miembro ?? 'Miembro',

cuota: vocab.value?.label_cuota ?? 'Cuota',

label_grupo: vocab.value?.label_grupo ?? 'Grupo',

}))



const esConsorcio = computed(() =>

vocab.value?.rubro_codigo === 'consorcio'

)



Resultado: el mismo componente DepartamentosView.vue muestra "Alumnos" o "Departamentos" o "Socios" según quien esté logueado, sin un solo if (rubro === 'escuela') en el template.





Cierre de mes automático



Uno de los features más pedidos: al cerrar el mes, generar automáticamente las deudas de quienes no pagaron.



python

@router.post("/{consorcio_id}/cerrar-mes")

async def cerrar_mes(consorcio_id: int, ...):

departamentos = await get_departamentos(consorcio_id)

ya_pagaron = []

ya_tienen_deuda = []

deudas_generadas = 0



for depto in departamentos:
pago = await get_pago_periodo(depto.id, periodo)

if pago:
ya_pagaron.append(depto.numero)
continue

deuda_existente = await get_deuda_activa(depto.id, periodo)
if deuda_existente:
ya_tienen_deuda.append(depto.numero)
continue

# Calcular monto con coeficiente
monto = consorcio.expensa_mensual * depto.coeficiente
db.add(Deuda(
departamento_id=depto.id,
periodo=periodo,
monto_original=monto,
monto_actual=monto,
))
deudas_generadas += 1

await db.commit()
return {
"deudas_generadas": deudas_generadas,
"ya_pagaron": len(ya_pagaron),
"ya_tienen_deuda": len(ya_tienen_deuda),
}



El bug más difícil acá: al principio mezclaba "ya pagaron" con "ya tienen deuda" en un solo contador. Llevaba a mensajes como "0 deudas generadas, 15 ya pagaron" cuando en realidad 14 ya pagaron y 1 ya tenía deuda de antes. Separar los casos fue simple pero tomó un rato entender por qué los números no cerraban.





Portal de pagos para inquilinos



Cada departamento tiene un public_token único. Con ese token, el inquilino accede a una vista pública donde ve su estado de cuenta y puede pagar con MercadoPago, sin necesidad de crear cuenta.



python

@router.get("/portal/{public_token}")

async def portal_publico(public_token: str, db: AsyncSession = Depends(get_db)):

result = await db.execute(

select(Departamento).where(Departamento.public_token == public_token)

)

depto = result.scalar_one_or_none()

if not depto:

raise HTTPException(404)



deudas = await get_deudas_activas(depto.id)
pagos = await get_pagos_recientes(depto.id)

return {
"departamento": depto.numero,
"deudas": deudas,
"pagos": pagos,
"puede_pagar_online": bool(depto.consorcio.mp_access_token)
}



El flujo de pago con MercadoPago:




  1. Frontend llama a POST /mp/crear-preferencia

  2. Backend crea la preferencia en la API de MP y devuelve init_point

  3. Redirigimos al usuario a esa URL

  4. MP callback a GET /mp/webhook cuando se aprueba

  5. Backend registra el pago y envía recibo por email con Resend





Importación masiva desde Excel



Para la incorporación inicial de datos, construí un endpoint que acepta un .xlsx y hace upsert por número de unidad:



python

@router.post("/{consorcio_id}/import/departamentos")

async def importar_departamentos(

consorcio_id: int,

file: UploadFile = File(...),

db: AsyncSession = Depends(get_db),

):

content = await file.read()

wb = load_workbook(filename=BytesIO(content), read_only=True, data_only=True)

ws = wb.active



existing = {d.numero: d for d in await get_departamentos(consorcio_id)}
created, updated, errors = 0, 0, []

for row_idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
numero = str(row[0]).strip() if row[0] else None
if not numero:
continue
if numero in existing:
# actualizar
updated += 1
else:
db.add(Departamento(consorcio_id=consorcio_id, numero=numero, ...))
created += 1

await db.commit()
return {"creados": created, "actualizados": updated, "errores": errors}



Y lo interesante: la plantilla descargable se adapta al rubro. Una escuela descarga una plantilla que dice "grado" en vez de "piso" y "alumno" en vez de "inquilino".





Freemium y feature flags



El modelo de negocio es freemium: plan gratuito con límite de 30 unidades, planes pagos con funcionalidades adicionales. Implementé un sistema de feature flags por plan:



python





En cada endpoint premium



@router.post("/{consorcio_id}/gastos")

async def crear_gasto(consorcio_id: int, ...):

plan = await get_plan(consorcio_id)

if not plan.tiene_extraordinarias:

raise HTTPException(403, "Funcionalidad no disponible en tu plan")



En el frontend, un store de features que bloquea la UI antes de que llegue al backend:



typescript

const featuresStore = useFeaturesStore()



// En el template

<button

@click="featuresStore.tiene('tiene_mp_integrado') ? abrirConfig() : irAPlanes()"

:class="!featuresStore.tiene('tiene_mp_integrado') ? 'opacity-50 cursor-not-allowed' : ''"




Configurar MercadoPago









Lo que aprendí



Sobre el stack:




  • FastAPI + SQLAlchemy async es una combinación excelente para APIs CRUD. La generación automática de OpenAPI docs ahorra horas.

  • Vue 3 Composition API con <script setup> es un placer de usar comparado con Options API.

  • Pinia es simple y predecible. No extraño Vuex.



Sobre el producto:




  • El vocabulario multi-rubro fue la decisión más inteligente. Hubiera necesitado 5 productos separados sin eso.

  • Los administradores de consorcios no son técnicos. El wizard de configuración inicial redujo drásticamente los mensajes de soporte.

  • El cierre de mes automático es el feature más usado. La gente literalmente esperaba hacer eso manual cada mes.



Sobre lanzar:




  • Empezar con un consorcio real (el propio) antes de monetizar fue clave para entender el flujo real.

  • Los primeros usuarios llegaron por boca a boca en grupos de WhatsApp de administradores.






Estado actual y próximos pasos



La plataforma está live en cuotia.com.ar con plan gratuito sin tarjeta de crédito.



Lo que viene:




  • Recordatorios automáticos por email para deudas vencidas

  • App mobile (PWA primero, nativa si hay demanda)

  • Migración de SQL manual a Alembic migrations (deuda técnica real)

  • API pública para integraciones con sistemas contables



Si estás construyendo algo similar o tenés preguntas sobre el stack, dejá un comentario. Y si administrás un consorcio, escuela o gimnasio en Argentina, probalo gratis.



Stack completo: FastAPI · SQLAlchemy · PostgreSQL · Vue 3 · Vite · TailwindCSS · Pinia · Docker · Nginx · MercadoPago · Resend



Tags: python vue fastapi saas argentina



No usé Next.js ni Nuxt porque quería separación clara entre API y frontend. FastAPI me da validación automática con Pydantic, docs en /docs sin esfuerzo y async nativo que necesitaba para las consultas concurrentes.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3

Thematisch verwandte Begriffe: Cómo, construí, SaaS, multirubro · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-82412 | ntopng is a web-based network traffic monitoring application. Prior to 6…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick