Saltar al contenido principal

Visión general del backend

El backend de MAMB combina Supabase (Postgres + Storage) con Server Actions de Next.js. Toda la lógica de negocio vive en el edge de confianza del servidor: nada se ejecuta en el cliente con datos no validados.

MAMB no tiene autenticación. Los visitantes suben su obra de forma anónima completando un formulario (título, autor, estilo y foto). El identificador del autor es solo un nombre libre.

Pilares

  1. Validación dura — Zod en cada Server Action antes de tocar la BD.
  2. Tipado de extremo a extremosupabase gen types typescript.
  3. RLS como red de seguridad — todas las tablas con RLS activo, INSERT público para permitir subidas anónimas.
  4. Rendimiento — paginación, índices, caché con revalidatePath.

Modelo de datos (resumen)

El schema canónico vive en supabase/schema.sql.

Server Actions

Patrón estándar para cualquier mutación:

'use server';

import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { createClient } from '@/lib/supabase/server';

const Input = z.object({
title: z.string().min(1).max(120),
authorName: z.string().min(1).max(80),
styleId: z.string().uuid(),
originalImageUrl: z.string().url(),
stylizedImageUrl: z.string().url().optional(),
description: z.string().max(500).optional(),
});

export async function crearObra(_: unknown, formData: FormData) {
const parsed = Input.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
return { success: false, error: 'Datos inválidos' };
}

const supabase = await createClient();

// 1. Upsert del autor anónimo por nombre
const { data: author, error: authorError } = await supabase
.from('authors')
.upsert({ name: parsed.data.authorName }, { onConflict: 'name' })
.select('id')
.single();

if (authorError || !author) {
return { success: false, error: 'No se pudo registrar el autor' };
}

// 2. Insert del artwork
const { error } = await supabase.from('artworks').insert({
title: parsed.data.title,
author_id: author.id,
style_id: parsed.data.styleId,
original_image_url: parsed.data.originalImageUrl,
stylized_image_url: parsed.data.stylizedImageUrl,
description: parsed.data.description,
});

if (error) {
return { success: false, error: 'No se pudo guardar la obra' };
}

revalidatePath('/gallery');
return { success: true };
}

Rate limiting

  • Máximo 5 obras por hora por IP (el usuario es anónimo).
  • Implementado con tabla rate_limit_buckets o en Edge KV.

Próximo paso