Supabase: (Backend como Servicio)#
Supabase es un Backend como Servicio (BaaS) que proporciona un conjunto de herramientas para desarrollar aplicaciones web sin la necesidad de implementar un backend completo. Este servicio incluye características como SDK, analíticas, autenticación, bases de datos, almacenamiento, dashboard, WebSockets, entre otros.
Supabase utiliza PostgreSQL como su motor de base de datos, permitiendo a los desarrolladores aprovechar todas las capacidades de esta robusta base de datos relacional.
Ofrece varios tipos de APIs
y un SDK
que facilita la interacción con el backend desde el cliente. También soporta el almacenamiento de archivos estáticos y permite la ejecución de funciones definidas por el usuario a través de RPC (Remote Procedure Call).
Supabase será útil en este curso porque nos permite tener un Backend sin programarlo. Lo configuraremos mínimamente para usar su
API REST
y crear aplicacionesSPA
. Puesto que hay que ser capaz de usar cualquierAPI Rest
, no usaremos el SDK de Supabase inicialmente.
Gestión de Tablas y API#
Cuando se crea una tabla en Supabase, se genera automáticamente una API para interactuar con ella. Puedes ver la API generada en el SDK o en Bash. Aquí utilizaremos ejemplos en Bash para evitar la instalación del SDK.
curl 'https://bqhnvwfovmcxfxr.supabase.co/rest/v1/graphs?select=id' \
-H "apikey: tu_apikey" \
-H "Authorization: Bearer tu_token"
Este comando curl
realiza una petición GET a la tabla graphs
para obtener el campo id
.
En JavaScript, se puede hacer una solicitud similar usando fetch
:
(()=>{
fetch('https://bqhnvwfovmcxfxr.supabase.co/rest/v1/graphs', {
method: 'GET',
headers: {
"apiKey": "tu_apikey",
"Authorization": "Bearer tu_token"
}
})
})();
Al igual que el ‘verbo’ GET
se pueden usar todos los demás de HTTP
para crear, actualizar, o borrar registros en la base de datos. Cómo usarlos está documentado en la API Docs
autogenerada por Supabase para cada tabla. Algunas operaciones más complejas como filtros avanzados no las documentan automáticamente, pero se pueden consultar en la documentación genérica.
Select#
Si no queremos todas las columnas o queremos los datos con otro formato, podemos pedir que realice un SELECT
, al fin y al cabo es una base de datos relacional. La sintaxis se puede ver en estos ejemplos:
https://bqhnvwfovmcxfxr.supabase.co/rest/v1/profiles?select=username,telefono,avatar_url,empresa:empresas(*),id
En este ejemplo obtenemos algunos datos típicos de un profile
y todos los datos de la empresa, la cual está relacionada con una clave que Supabase conoce. Esto producirá una subconsulta y no solo retornará el id
, que es lo que hay en la columna empresa
, si no también otros datos.
https://bqhnvwfovmcxfxr.supabase.co/rest/v1/empresas?select=nombre,dirección,telefono,c_postal,localidad,cif,email,web,presidente,coordinadores:coordinacion_empresa(profiles(id,username)),socios:empresas_socios(socios(id,nombre)),id
En este ejemplo hay un SELECT
complejo en el que se piden datos que están en la tabla, pero luego hay una subconsulta en la que se piden los coordinadores de esa empresa, que están en una tabla intermedia coordinacion_empresa
. Como Supabase conoce las claves ajenas, retorna los profiles
que están en esa relación muchos a muchos. Además, de estos extrae el id
y el username
. Lo mismo para con los socios
, la tabla intermedia empresas_socios
y la tabla socios
, de la que queremos el id y el nombre.
https://bqhnvwfovmcxfxr.supabase.co/rest/v1/profiles?select=*&id=eq.8662025a-25be-4777-ae54-66cb6111129e
En este ejemplo ponemos una condición simple de búsqueda, lo que será el WHERE
en la consulta. En ella pedimos que el id
cumpla la condición de ser igual (eq
) que un uuid
determinado separado por un punto.
Se pueden poner condiciones más complejas. En ese caso es mejor consultar la documentación, pero aquí tenemos algún ejemplo:
Pedir la cantidad (count): Método
HEAD
yprefer: count=exact
en el header. El resultado se recibe en el headercontent-range
.Consultas con “like”:
products?select=*&identifier=like.%AX%
Consultas con OR lógico:
select=*&or=(id.eq.464, id.eq.466)
En cualquier caso, si queremos saber cómo se traduce una petición en la API REST, podemos usar esta herramienta: https://supabase.com/docs/guides/api/sql-to-rest
POST#
Sirve para crear nuevos registros en una tabla. En la URL se pone la tabla, el verbo POST
y en el BODY
se ponen los datos en formato JSON
.
let response = await fetch(`https://bqhnvwfovmcxfxr.supabase.co/rest/v1/movies`,{
method: `POST`,
headers: {
"apiKey": SUPABASE_KEY,
"Content-Type": "application/json",
"Authorization" :"Bearer "+token,
"Prefer" : "return=representation"
};
body: JSON.stringify(body);
});
PATCH#
Funciona igual que el POST
,pero hay que indicar en la URL el id
o otro campo de filtro para modificar los que cumplan esa condición:
let response = await fetch(`https://bqhnvwfovmcxfxr.supabase.co/rest/v1/movies?id=eq.${id}`,{
method: `PATCH`,
headers: {
"apiKey": SUPABASE_KEY,
"Content-Type": "application/json",
"Authorization" :"Bearer "+token,
"Prefer" : "return=representation"
};
body: JSON.stringify(body);
});
Además, tenemos DELETE
con la sintaxis de PATCH
para eliminar.
Almacenamiento de JSON#
En Supabase, el primer nivel de los objetos se guarda como columnas en la tabla. Si los objetos tienen subniveles o se realizan consultas a otras tablas, se guardan en formato JSON como texto.
Autenticación#
Las tablas públicas son accesibles con la apiKey pública. Sin embargo, al estar en el cliente, cualquiera podría ver esta apiKey en el código. Para proteger tablas privadas, se deben crear usuarios y aplicar métodos de autenticación y autorización.
En resumen, para implementar la autenticación en Supabase hay que seguir unos pasos:
Habilitar o asegurarse de que está habilitada la autenticación por contraseña.
Ir a la parte de
SQL Editor
y ejecutar elQuickstart
User Management Starter
. Esto no es preciso para la autenticación, pero prepara la base de datos para mantener los perfiles de los usuarios y sus avatares. Si lo hacemos desde el principio, todos los usuarios funcionarán correctamente.Ir al
API Docs
y mirar cómo se hace la autenticación porBash
.Crear funciones para autenticar y registrar usuarios con el método descrito en el paso anterior.
Crear formularios para llamar a esas funciones.
Guardar el
token
después de la autenticación enLocalstorage
.Modificar las funciones ya creadas para usar el
token
en cada petición.Modificar las
RLS
de las tablas para protegerlas de las peticiones que no tengan el token.Aplicar funciones para gestionar la duración de las sesiones, el logout…
Gestionar los perfiles de usuario como una tabla más llamada
profiles
y un bucketavatars
creados automáticamente con elUser Management Starter
.
Activación de RLS (Row Level Security)#
Primero, es necesario activar RLS en las tablas y crear políticas para controlar el acceso. Por ejemplo, podemos permitir que solo los usuarios autenticados puedan leer y escribir en una tabla.
Las
RLS
son muy interesantes y, por si solas casi permiten configurar el comportamiento de un backend completo. No obstante, por cuestión de tiempo y enfoque, para el curso las haremos simples y permisivas para todos los usuarios con la única condición de que estén autenticados.
Autenticación por Email y Contraseña#
https://www.youtube.com/watch?v=pi33WDrgfpI
Podemos invitar usuarios a través de la interfaz o crear un formulario de inscripción. En cualquier caso, es necesario que el usuario confirme su email.
En el caso de la invitación, la redirección por defecto es a localhost:3000
, pero esto se puede modificar. Una vez confirmado, el usuario recibirá un token que se puede probar en jwt.io. Este token se usará en la cabecera Authorization: Bearer
.
El token lo podemos guardar en localstorage
para recuperar cada vez que se haga una petición.
Gestión del Tiempo de Sesión#
Después de iniciar sesión, guardamos el token en LocalStorage
para realizar peticiones. Cada inicio de sesión retorna un tiempo de expiración (expires_in
y expires_at
). Podemos calcular la fecha de expiración y evitar confiar en el token después de esta fecha.
Gestión de Perfiles de Usuario#
Es común que los usuarios necesiten gestionar información personal como avatar, dirección, teléfono, etc. Para ello, Supabase proporciona un script SQL (Quickstart
User Management Starter
) que crea una tabla de perfiles de usuario y asocia la información correspondiente. Este script crea unos triggers
en la base de datos para crear una fila en profiles
cada vez que un usuario se registra.
Cuando un usuario inicia sesión, obtenemos sus datos de perfil y los almacenamos en LocalStorage
:
async function getProfile(){
let access_token = localStorage.getItem('access_token');
let uid = localStorage.getItem('uid');
let responseGet = await getData(`profiles?id=eq.${uid}&select=*`,access_token); // Función que simplifica el fetch
let avatar_url = responseGet[0].avatar_url;
responseGet[0].avatar_blob = false;
if (avatar_url){
let imageBlob = await getFileRequest(avatar_url,access_token); // Función que simplifica el fetch a un bucket.
console.log(imageBlob);
if(imageBlob instanceof Blob){
responseGet[0].avatar_blob = URL.createObjectURL( imageBlob );
}
}
return responseGet;
}
Esta función, además, pide el fichero del avatar.
Gestión de Avatares#
Los avatares se guardan en “Storage”. Es necesario crear un bucket con permisos de escritura y actualización para los usuarios autenticados. Para subir imágenes, se realiza un POST con un FormData
que contiene la imagen.
async function updateProfile(profile) {
let access_token = localStorage.getItem('access_token');
let uid = localStorage.getItem('uid');
let formImg = new FormData();
formImg.append("avatar", profile.avatar, 'avatarProfile.png');
let avatarResponse = await fileRequest(`/storage/v1/object/avatars/avatar${uid}.png`, formImg, access_token);
profile.avatar_url = avatarResponse.urlAvatar;
delete profile.avatar;
let responseUpdate = await updateData(`profiles?id=eq.${uid}&select=*`, access_token, profile);
}
Subida de una Imagen a Storage#
const headersFile = {
"apiKey": SUPABASE_KEY,
"Authorization": `Bearer ${token}`,
"x-upsert": true
};
let response = await fetch(`${urlBase}${url}`, {
method: 'POST',
headers: headersFile,
body: formImg
});
if (response.status >= 200 && response.status <= 300) {
if (response.headers.get("content-type")) {
let datos = await response.json();
datos.urlAvatar = `${urlBase}${url}`;
return datos;
}
return {};
}
Descarga de una Imagen#
Si no se necesita autorización, se puede utilizar un bucket público y la URL proporcionada. Si se requiere autorización, se debe enviar la API key y el token.
const headersFile = {
"apiKey": SUPABASE_KEY,
"Authorization": `Bearer ${token}`
};
let response = await fetch(`${url}`, {
method: 'GET',
headers: headersFile
});
if (response.status >= 200 && response.status <= 300) {
if (response.headers.get("content-type")) {
let datos = await response.blob();
return datos;
}
return {};
}
let avatar_url = responseGet[0].avatar_url;
responseGet[0].avatar_blob = URL.createObjectURL(await getFileRequest(avatar_url, access_token));
WebSocket#
Supabase ofrece una función llamada Realtime para manejar WebSockets. Se debe agregar en la sección de Database > Replication las tablas que queremos monitorear. Es muy recomendable utilizar las librerías proporcionadas por Supabase para esto.
https://supabase.com/docs/guides/realtime/postgres-changes
Ejemplo de Comunicación “Broadcast”#
Supabase permite la comunicación directa con otros usuarios o la notificación de cambios en las tablas mediante WebSockets.
const supabaseRealTimeClient = supabase.createClient(supabaseUrl, SUPABASE_KEY);
const currentUser = Math.random()+"";
const channel = supabaseRealTimeClient.channel('chat-db')
function renderMessage(message){
const messageContainer = document.getElementById('message-container');
const messageElement = document.createElement('div');
messageElement.classList.add('message');
messageElement.classList.add(message.user === currentUser ? 'outgoing' : 'incoming');
const messageContent = document.createElement('span');
messageContent.classList.add('message-content');
messageContent.textContent = message.message;
messageElement.appendChild(messageContent);
messageContainer.appendChild(messageElement);
// Hacer scroll hacia abajo para mostrar el último mensaje
messageContainer.scrollTop = messageContainer.scrollHeight;
}
channel
.on(
'postgres_changes', {
event: '*',
schema: 'public',
table: 'chat',
}, (payload) => {
console.log(payload);
renderMessage(payload.new)
}).subscribe((status) => console.log(status))
async function downloadHistoric(){
const { data, error } = await supabaseRealTimeClient.from("chat").select();
// console.log(data);
data.forEach(renderMessage);
}
document.addEventListener("DOMContentLoaded",()=>{
downloadHistoric();
document.querySelector("#send-button").addEventListener("click", async ()=>{
let message = document.querySelector('#message-input').value;
const { data, error } = await supabaseRealTimeClient.from("chat").insert([
{
user: currentUser,
message: message
},
]);
});
})
Ejemplo de Chat con Mensajes Guardados#
Supabase facilita la implementación de un sistema de chat donde los mensajes se guardan en la base de datos. Puedes modificar el chat para mantener la sesión del usuario en cookies.
const supabaseRealTimeClient = supabase.createClient(supabaseUrl, SUPABASE_KEY);
const currentUser = Math.random()+"";
const channel = supabaseRealTimeClient.channel('chat', {
config: {
broadcast: {
self: true,
},
},
})
channel.subscribe()
.on('broadcast', { event: 'supa' }, (payload) => {
console.log(payload);
const messageContainer = document.getElementById('message-container');
const messageElement = document.createElement('div');
messageElement.classList.add('message');
messageElement.classList.add(payload.payload.user === currentUser ? 'outgoing' : 'incoming');
const messageContent = document.createElement('span');
messageContent.classList.add('message-content');
messageContent.textContent = payload.payload.message;
messageElement.appendChild(messageContent);
messageContainer.appendChild(messageElement);
// Hacer scroll hacia abajo para mostrar el último mensaje
messageContainer.scrollTop = messageContainer.scrollHeight;
})
document.addEventListener("DOMContentLoaded",()=>{
document.querySelector("#send-button").addEventListener("click",()=>{
let message = document.querySelector('#message-input').value;
channel.send({
type: 'broadcast',
event: 'supa',
payload: {message,user:currentUser},
})
});
})