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
- Validación dura — Zod en cada Server Action antes de tocar la BD.
- Tipado de extremo a extremo —
supabase gen types typescript. - RLS como red de seguridad — todas las tablas con RLS activo,
INSERTpúblico para permitir subidas anónimas. - 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_bucketso en Edge KV.