← Back to list

Flutter + Supabase: cómo crear apps modernas sin construir un backend desde cero

Una guía práctica para conectar tu app Flutter con autenticación, base de datos y datos en tiempo real

Gian Sandoval · 2026-05-25 15:48 · 0 claps · 7.3 min read
#flutter-app-development #flutter #supabase #mobile-app-development #dart
Open on Medium ↗
Wiki topics: 🌐 · Web Development 📱 · Mobile Development

Flutter + Supabase: cómo crear apps modernas sin construir un backend desde cero

Una guía práctica para conectar tu app Flutter con autenticación, base de datos y datos en tiempo real

Durante mucho tiempo, crear una aplicación móvil completa significaba trabajar en dos mundos separados: por un lado, la interfaz en mobile; por otro, el backend con servidores, base de datos, autenticación, seguridad, despliegues y mantenimiento.

Flutter resolvió gran parte del problema del frontend: nos permite crear aplicaciones multiplataforma desde una sola base de código, con una experiencia visual muy potente y una curva de desarrollo bastante productiva. La documentación oficial lo presenta justamente como una forma de construir apps para múltiples plataformas desde un único codebase. (docs.flutter.dev)

Pero aún queda una pregunta clave:

¿Qué usamos para el backend?

Ahí entra Supabase.

Supabase es una excelente alternativa cuando queremos avanzar rápido sin sacrificar arquitectura. Nos da una base de datos PostgreSQL, autenticación, almacenamiento, funciones, APIs y capacidades en tiempo real. Además, su librería para Flutter permite interactuar con base de datos, Auth, Realtime, Edge Functions y Storage desde una app Flutter. (Supabase)

En este artículo veremos cómo conectar Flutter con Supabase y construir la base de una pequeña app de tareas.

¿Por qué Flutter y Supabase hacen tan buen equipo?

Flutter destaca cuando queremos crear interfaces rápidas, expresivas y consistentes. Supabase destaca cuando necesitamos un backend funcional sin comenzar desde cero.

La combinación funciona muy bien porque:

  • Flutter se encarga de la experiencia del usuario.
  • Supabase se encarga de la base de datos, autenticación, APIs y seguridad.
  • PostgreSQL nos da una base sólida y escalable.
  • Realtime permite escuchar cambios de la base de datos desde la app.
  • Row Level Security permite controlar qué datos puede ver o modificar cada usuario.

En otras palabras, Flutter nos ayuda a construir el producto visible; Supabase nos ayuda a construir la infraestructura invisible.

Qué vamos a construir

Vamos a crear la base de una app simple de tareas donde cada usuario pueda:

  • iniciar sesión,
  • ver sus tareas,
  • crear nuevas tareas,
  • marcar tareas como completadas,
  • mantener los datos en Supabase.

La idea no es construir una app enorme, sino entender la arquitectura mínima para comenzar un proyecto real.

1. Crear el proyecto Flutter

Primero creamos una nueva aplicación:

flutter create flutter_supabase_tasks
cd flutter_supabase_tasks

Luego agregamos la librería de Supabase para Flutter:

flutter pub add supabase_flutter

La guía oficial de Supabase recomienda usar supabase_flutter como cliente principal para trabajar con Supabase desde Flutter. (Supabase)

2. Crear el proyecto en Supabase

Desde el dashboard de Supabase, creamos un nuevo proyecto. Luego necesitaremos dos valores:

SUPABASE_URL
SUPABASE_PUBLISHABLE_KEY

Estos valores se obtienen desde el panel del proyecto, normalmente en la sección de conexión o API Keys.

Un detalle importante: la referencia actual de Supabase recomienda inicializar el cliente usando publishableKey. El parámetro anterior anonKey todavía puede funcionar, pero publishableKey tiene prioridad cuando ambos están presentes. (Supabase)

3. Inicializar Supabase en Flutter

Abrimos lib/main.dart y configuramos Supabase antes de ejecutar la app:

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Supabase.initialize(
    url: 'TU_SUPABASE_URL',
    publishableKey: 'TU_SUPABASE_PUBLISHABLE_KEY',
  );
  runApp(const MyApp());
}
final supabase = Supabase.instance.client;
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter + Supabase',
      theme: ThemeData(
        useMaterial3: true,
      ),
      home: const TasksPage(),
    );
  }
}

Con esto, nuestra app ya tiene acceso al cliente de Supabase.

4. Crear la tabla de tareas en Supabase

Ahora vamos a crear una tabla llamada tasks.

En el SQL Editor de Supabase podemos ejecutar:

create table public.tasks (
  id bigint generated by default as identity primary key,
  user_id uuid not null default auth.uid() references auth.users(id) on delete cascade,
  title text not null,
  is_done boolean not null default false,
  created_at timestamptz not null default now()
);

alter table public.tasks enable row level security;
grant select, insert, update, delete on public.tasks to authenticated;

Aquí estamos creando una tabla donde cada tarea pertenece a un usuario. El campo user_id se llena por defecto usando auth.uid(), es decir, el ID del usuario autenticado.

También activamos Row Level Security, o RLS. Esto es clave: en una app real, no basta con tener una tabla; necesitamos asegurarnos de que cada usuario solo pueda acceder a sus propios datos.

La documentación de Supabase recomienda habilitar RLS y crear políticas antes de conceder permisos a roles cliente. (Supabase)

Ahora agregamos las políticas:

create policy "Users can read their own tasks"
on public.tasks
for select
to authenticated
using (auth.uid() = user_id);

create policy "Users can insert their own tasks"
on public.tasks
for insert
to authenticated
with check (auth.uid() = user_id);
create policy "Users can update their own tasks"
on public.tasks
for update
to authenticated
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
create policy "Users can delete their own tasks"
on public.tasks
for delete
to authenticated
using (auth.uid() = user_id);

Con esto, Supabase no solo guarda datos, sino que también protege el acceso a nivel de fila.

5. Autenticación simple con Magic Link

Una forma rápida de comenzar es usando autenticación por email con OTP o Magic Link.

Supabase permite iniciar sesión con OTP enviando un código o enlace al correo del usuario. (Supabase)

Podemos crear una pantalla sencilla:

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});
@override
  State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
  final emailController = TextEditingController();
  bool loading = false;
  Future<void> signIn() async {
    setState(() => loading = true);
    try {
      await supabase.auth.signInWithOtp(
        email: emailController.text.trim(),
      );
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Revisa tu correo para iniciar sesión.'),
        ),
      );
    } catch (error) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Error: $error')),
      );
    } finally {
      if (mounted) {
        setState(() => loading = false);
      }
    }
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Text(
                'Inicia sesión',
                style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 24),
              TextField(
                controller: emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: const InputDecoration(
                  labelText: 'Correo electrónico',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: loading ? null : signIn,
                child: Text(loading ? 'Enviando...' : 'Enviar Magic Link'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Este flujo es ideal para prototipos, MVPs o aplicaciones donde queremos evitar manejar contraseñas desde el inicio.

6. Leer tareas desde Supabase

Supabase nos permite hacer consultas tipo select() directamente desde Flutter. Según la referencia oficial, select() se usa para consultar una tabla o vista, y también puede combinarse con filtros y modificadores. (Supabase)

Podemos obtener las tareas así:

Future<List<Map<String, dynamic>>> getTasks() async {
  final data = await supabase
      .from('tasks')
      .select()
      .order('created_at', ascending: false);
return List<Map<String, dynamic>>.from(data);
}

Luego podemos mostrar los datos en pantalla:

class TasksPage extends StatefulWidget {
  const TasksPage({super.key});
@override
  State<TasksPage> createState() => _TasksPageState();
}
class _TasksPageState extends State<TasksPage> {
  final taskController = TextEditingController();
  Future<List<Map<String, dynamic>>> getTasks() async {
    final data = await supabase
        .from('tasks')
        .select()
        .order('created_at', ascending: false);
    return List<Map<String, dynamic>>.from(data);
  }
  Future<void> addTask() async {
    final title = taskController.text.trim();
    if (title.isEmpty) return;
    await supabase.from('tasks').insert({
      'title': title,
    });
    taskController.clear();
    setState(() {});
  }
  Future<void> toggleTask(int id, bool currentValue) async {
    await supabase
        .from('tasks')
        .update({'is_done': !currentValue})
        .eq('id', id);
    setState(() {});
  }
  Future<void> deleteTask(int id) async {
    await supabase
        .from('tasks')
        .delete()
        .eq('id', id);
    setState(() {});
  }
  @override
  Widget build(BuildContext context) {
    final user = supabase.auth.currentUser;
    if (user == null) {
      return const LoginPage();
    }
    return Scaffold(
      appBar: AppBar(
        title: const Text('Mis tareas'),
        actions: [
          IconButton(
            onPressed: () async {
              await supabase.auth.signOut();
              setState(() {});
            },
            icon: const Icon(Icons.logout),
          ),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: taskController,
                    decoration: const InputDecoration(
                      labelText: 'Nueva tarea',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 12),
                ElevatedButton(
                  onPressed: addTask,
                  child: const Text('Agregar'),
                ),
              ],
            ),
          ),
          Expanded(
            child: FutureBuilder<List<Map<String, dynamic>>>(
              future: getTasks(),
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return const Center(
                    child: CircularProgressIndicator(),
                  );
                }
                if (snapshot.hasError) {
                  return Center(
                    child: Text('Error: ${snapshot.error}'),
                  );
                }
                final tasks = snapshot.data ?? [];
                if (tasks.isEmpty) {
                  return const Center(
                    child: Text('No tienes tareas todavía.'),
                  );
                }
                return ListView.builder(
                  itemCount: tasks.length,
                  itemBuilder: (context, index) {
                    final task = tasks[index];
                    return ListTile(
                      leading: Checkbox(
                        value: task['is_done'] as bool,
                        onChanged: (_) {
                          toggleTask(
                            task['id'] as int,
                            task['is_done'] as bool,
                          );
                        },
                      ),
                      title: Text(
                        task['title'] as String,
                        style: TextStyle(
                          decoration: task['is_done'] == true
                              ? TextDecoration.lineThrough
                              : null,
                        ),
                      ),
                      trailing: IconButton(
                        icon: const Icon(Icons.delete),
                        onPressed: () {
                          deleteTask(task['id'] as int);
                        },
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

Supabase permite insertar datos con insert() y actualizar registros con update(), combinando filtros como .eq() para modificar elementos específicos. (Supabase)

7. ¿Y si queremos datos en tiempo real?

Una de las partes más interesantes de Supabase es Realtime. En lugar de consultar manualmente cada cierto tiempo, podemos escuchar cambios en una tabla.

La referencia de Supabase indica que stream() devuelve datos en tiempo real como un Stream<List<Map<String, dynamic>>>, aunque Realtime está deshabilitado por defecto en tablas nuevas y debe activarse desde la configuración correspondiente. (Supabase)

Un ejemplo básico sería:

final tasksStream = supabase
    .from('tasks')
    .stream(primaryKey: ['id'])
    .order('created_at', ascending: false);

Y luego usarlo con StreamBuilder:

StreamBuilder<List<Map<String, dynamic>>>(
  stream: tasksStream,
  builder: (context, snapshot) {
    if (!snapshot.hasData) {
      return const Center(child: CircularProgressIndicator());
    }

final tasks = snapshot.data!;
    return ListView.builder(
      itemCount: tasks.length,
      itemBuilder: (context, index) {
        final task = tasks[index];
        return ListTile(
          title: Text(task['title']),
        );
      },
    );
  },
);

Esto abre posibilidades muy interesantes: chats, dashboards, tableros colaborativos, notificaciones internas o cualquier interfaz que necesite reaccionar rápidamente a cambios en la base de datos.

Buenas prácticas al usar Flutter + Supabase

Aunque Supabase nos permite avanzar rápido, hay algunos puntos importantes que debemos considerar:

1. No expongas claves secretas en Flutter

En una app cliente solo debe usarse la clave pública o publishable key. Las claves secretas deben mantenerse en entornos seguros, como servidores o Edge Functions.

2. Activa Row Level Security

No construyas una app real dejando tablas abiertas sin políticas. RLS es una de las piezas más importantes para proteger los datos de tus usuarios.

3. Separa tu lógica

Aunque puedes llamar a Supabase directamente desde tus widgets, en proyectos más grandes conviene separar responsabilidades:

UI -> Controller / State Management -> Repository -> Supabase

Esto hace que tu código sea más mantenible y fácil de testear.

4. Maneja estados de carga y errores

Una app profesional no solo muestra datos. También debe manejar:

  • loading,
  • errores,
  • estados vacíos,
  • reconexión,
  • permisos,
  • sesiones expiradas.

5. Usa Edge Functions cuando necesites lógica sensible

Si necesitas ejecutar lógica que no debe vivir en el cliente, como pagos, validaciones avanzadas o llamadas a APIs privadas, no la pongas en Flutter. Muévela a una función del lado del servidor.

Conclusión

Flutter y Supabase son una combinación muy poderosa para construir productos modernos.

Flutter nos permite crear interfaces atractivas y multiplataforma. Supabase nos da un backend real con PostgreSQL, autenticación, almacenamiento, APIs y capacidades en tiempo real.

Lo más interesante es que esta combinación no solo sirve para prototipos. Con buenas prácticas, Row Level Security, separación de capas y un manejo adecuado de credenciales, también puede ser la base de aplicaciones listas para producción.

Si estás construyendo un MVP, una app para una startup, una herramienta interna o un producto personal, Flutter + Supabase puede ayudarte a pasar de la idea a una primera versión funcional mucho más rápido.


메타데이터
post_id
1a7de94367cb
slug
flutter-supabase-cómo-crear-apps-modernas-sin-construir-un-backend-desde-cero-1a7de94367cb
url
https://medium.com/@giansando2022/flutter-supabase-c%C3%B3mo-crear-apps-modernas-sin-construir-un-backend-desde-cero-1a7de94367cb
canonical_url
https://medium.com/@giansando2022/flutter-supabase-c%C3%B3mo-crear-apps-modernas-sin-construir-un-backend-desde-cero-1a7de94367cb
author_url
https://medium.com/@giansando2022
status
ok
fetched_at
2026-06-09 14:34:10