[{"data":1,"prerenderedAt":1335},["ShallowReactive",2],{"blog-es-null-value-violates-not-null-constraint":3,"blog-index-es":809},{"id":4,"title":5,"body":6,"category":767,"cover":768,"coverAlt":769,"description":770,"draft":771,"extension":772,"faq":773,"image":789,"keywords":790,"meta":798,"navigation":799,"ogTitle":5,"path":800,"published":801,"seo":802,"stem":803,"tldr":804,"updated":801,"__hash__":808},"blog_es\u002Fblog\u002Fnull-value-violates-not-null-constraint.md","Error de Supabase \"null value in column violates not-null constraint\"",{"type":7,"value":8,"toc":755},"minimark",[9,13,24,35,38,43,46,49,171,180,184,187,201,216,241,258,293,308,312,321,350,367,375,381,387,394,400,404,407,420,426,429,435,443,448,452,455,463,474,484,490,496,514,544,548,551,562,569,572,596,614,617,623,626,632,639,644,648,651,660,682,691,705,709,748],[10,11,12],"p",{},"Pulsas Guardar en una tarea nueva de tu app y no se guarda nada. La página no\nmuestra ningún error y la lista sigue igual. La consola del navegador tiene el\nmotivo:",[14,15,20],"pre",{"className":16,"code":18,"language":19},[17],"language-text","{\n  \"code\": \"23502\",\n  \"details\": null,\n  \"hint\": null,\n  \"message\": \"null value in column \\\"category_id\\\" of relation \\\"tasks\\\" violates not-null constraint\"\n}\n","text",[21,22,18],"code",{"__ignoreMap":23},"",[10,25,26,27,30,31,34],{},"Supabase \"null value in column violates not-null constraint\" significa que una\nfila llegó a tu base de datos sin nada en una columna que siempre debe tener un\nvalor, y Postgres rechazó la fila entera. Aquí la columna es ",[21,28,29],{},"category_id"," en tu\ntabla ",[21,32,33],{},"tasks",". Al guardar no se envió ninguna categoría.",[10,36,37],{},"El arreglo más rápido es quitar la regla: una línea de SQL y la columna puede\nquedarse vacía. El error desaparece, y a partir de ahí llegan filas con esa\ncasilla en blanco, que el resto de tu app encuentra donde no estabas mirando.\nAverigua primero de dónde salió el valor vacío. Casi siempre es uno de unos pocos\nsitios.",[39,40,42],"h2",{"id":41},"qué-significa-null-value-in-column-violates-not-null-constraint","¿Qué significa \"null value in column violates not-null constraint\"?",[10,44,45],{},"Significa que una columna de la fila que tu app quiso guardar estaba vacía, que\nesa columna nunca puede estarlo, y que Postgres se negó a guardar nada de la\nfila. No se escribió nada. Null es la palabra de Postgres para la ausencia de\nvalor, que no es lo mismo que un cero ni que un texto sin letras.",[10,47,48],{},"Piensa en cada fila como un formulario de papel y en la tabla como el cajón del\narchivador donde se guarda. Una restricción not-null es un asterisco impreso\njunto a una casilla. Postgres está junto al cajón y no archiva ningún formulario\nque tenga vacía una casilla con asterisco. El formulario vuelve, y el mensaje\ndice qué casilla.",[50,51,52,68],"table",{},[53,54,55],"thead",{},[56,57,58,62,65],"tr",{},[59,60,61],"th",{},"Campo",[59,63,64],{},"Ejemplo",[59,66,67],{},"Qué te dice",[69,70,71,97,112,131,145],"tbody",{},[56,72,73,78,83],{},[74,75,76],"td",{},[21,77,21],{},[74,79,80],{},[21,81,82],{},"23502",[74,84,85,86,89,90],{},"El código de Postgres para este error, llamado ",[21,87,88],{},"not_null_violation"," en ",[91,92,96],"a",{"href":93,"rel":94},"https:\u002F\u002Fgithub.com\u002Fpostgres\u002Fpostgres\u002Fblob\u002Fmaster\u002Fsrc\u002Fbackend\u002Futils\u002Ferrcodes.txt",[95],"nofollow","su lista de códigos",[56,98,99,104,109],{},[74,100,101],{},[21,102,103],{},"message",[74,105,106],{},[21,107,108],{},"null value in column \"category_id\" of relation \"tasks\" violates not-null constraint",[74,110,111],{},"La columna vacía y luego la tabla. \"Relation\" es la palabra de Postgres para una tabla",[56,113,114,119,128],{},[74,115,116],{},[21,117,118],{},"details",[74,120,121,124,125],{},[21,122,123],{},"Failing row contains (…)",", o ",[21,126,127],{},"null",[74,129,130],{},"Una copia de la fila rechazada, cuando Postgres decide que quien llama puede verla. Una sección más abajo explica cuándo",[56,132,133,138,142],{},[74,134,135],{},[21,136,137],{},"hint",[74,139,140],{},[21,141,127],{},[74,143,144],{},"Nada que añadir para este error",[56,146,147,150,155],{},[74,148,149],{},"Estado HTTP",[74,151,152],{},[21,153,154],{},"400 Bad Request",[74,156,157,158,161,162,164,165,170],{},"PostgREST, la parte de Supabase que convierte las llamadas ",[21,159,160],{},"supabase.from(...)"," en SQL, no tiene un caso propio para ",[21,163,82],{},", así que ",[91,166,169],{"href":167,"rel":168},"https:\u002F\u002Fgithub.com\u002FPostgREST\u002Fpostgrest\u002Fblob\u002Fmain\u002Fsrc\u002Flibrary\u002FPostgREST\u002FError.hs",[95],"su código"," cae en 400",[10,172,173,174,179],{},"El texto es el de Postgres, de\n",[91,175,178],{"href":176,"rel":177},"https:\u002F\u002Fgithub.com\u002Fpostgres\u002Fpostgres\u002Fblob\u002Fmaster\u002Fsrc\u002Fbackend\u002Fexecutor\u002FexecMain.c",[95],"la función que lanza el error",",\ny se lee igual tanto si la fila vino de tu app como de un trigger o del SQL Editor.",[39,181,183],{"id":182},"de-dónde-salió-el-valor-vacío","¿De dónde salió el valor vacío?",[10,185,186],{},"Normalmente de uno de tres sitios en el código de tu propia app, y la columna del\nmensaje te dice dónde mirar primero. Otros dos empiezan dentro de la base de\ndatos.",[10,188,189,193,194,196,197,200],{},[190,191,192],"strong",{},"Un campo que tu app nunca envía."," Es el caso más común. La columna pasó a ser\nobligatoria después de crear el formulario: un constructor con IA añadió\n",[21,195,29],{}," a la tabla cuando pediste categorías, la marcó como obligatoria y\ndejó como estaba el formulario que guarda las tareas. Nada en la petición nombra\nla columna, la columna no tiene valor por defecto, así que queda vacía. Compara\nlas columnas de tu Table Editor con el objeto que tu app pasa a ",[21,198,199],{},".insert()",". La\nque falta es la del mensaje.",[10,202,203,204,207,208,210,211,215],{},"Una clave escrita distinto que su columna ni siquiera llega hasta aquí. PostgREST\nrechaza ",[21,205,206],{},"categoryId"," enviado a una columna llamada ",[21,209,29],{}," antes de que\nPostgres vea la fila, y\n",[91,212,214],{"href":213},"\u002Fblog\u002Fsupabase-schema-cache-column-error","ese error tiene su propio artículo",".",[10,217,218,221,222,225,226,229,230,237,238,240],{},[190,219,220],{},"Un null que tu código envía a propósito."," ",[21,223,224],{},"{ category_id: selected?.id ?? null }","\nenvía un null explícito cuando no hay nada seleccionado, y un null explícito es un\nvalor para la petición. El ",[21,227,228],{},"undefined"," de JavaScript se comporta de otra forma.\nJSON no tiene ese valor, así que\n",[91,231,234],{"href":232,"rel":233},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FJavaScript\u002FReference\u002FGlobal_Objects\u002FJSON\u002Fstringify",[95],[21,235,236],{},"JSON.stringify","\nlo deja \"omitted (when found in an object)\", y una clave con ",[21,239,228],{}," nunca\nllega a Supabase. Cuál de los dos produce tu código decide si un valor por defecto\npuede entrar en juego, y de eso trata la siguiente sección.",[10,242,243,221,246,249,250,253,254,257],{},[190,244,245],{},"Una columna sin el valor por defecto que dabas por hecho.",[21,247,248],{},"created_at"," suele\nrellenarse sola, porque la tabla dice ",[21,251,252],{},"default now()",". ",[21,255,256],{},"user_id"," normalmente no,\nporque nada en Postgres sabe quién ha iniciado sesión hasta que le dices a la\ncolumna cómo averiguarlo. Una sección más adelante muestra cómo.",[10,259,260,263,264,267,268,271,272,277,278,281,282,284,285,289,290,292],{},[190,261,262],{},"Un registro, bajo otro mensaje."," Un trigger que copia cada usuario nuevo en una\ntabla ",[21,265,266],{},"profiles"," lee sus datos de registro con ",[21,269,270],{},"->>",", y\n",[91,273,276],{"href":274,"rel":275},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Ffunctions-json.html",[95],"los operadores JSON de Postgres","\n\"return NULL, rather than failing, if the JSON input does not have the right\nstructure to match the request; for example if no such key or array element\nexists\". Un formulario de registro que nunca envió ",[21,279,280],{},"full_name"," le da al trigger un\nnombre vacío, una columna obligatoria ",[21,283,280],{}," lo rechaza, y Supabase informa\nde todo el registro como\n",[91,286,288],{"href":287},"\u002Fblog\u002Fdatabase-error-saving-new-user","\"Database error saving new user\"",". El\n",[21,291,82],{}," en sí está en los registros de Postgres, y ese artículo muestra dónde.",[10,294,295,298,299,302,303,307],{},[190,296,297],{},"Un borrado en otra parte."," Una clave foránea con ",[21,300,301],{},"on delete set null"," vacía la\ncolumna en cada fila que apuntaba a la borrada. Si esa columna también es\nobligatoria, lo que falla con este mensaje es el borrado, y\n",[91,304,306],{"href":305},"\u002Fblog\u002Fdatabase-error-deleting-user","el artículo sobre borrar usuarios"," explica\ncómo elegir una regla que encaje.",[39,309,311],{"id":310},"pero-la-columna-tiene-un-valor-por-defecto","\"Pero la columna tiene un valor por defecto\"",[10,313,314,315,320],{},"Entonces la petición nombró la columna y la envió vacía. Postgres aplica un valor\npor defecto solo cuando una columna queda fuera de la fila por completo.\n",[91,316,319],{"href":317,"rel":318},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fddl-default.html",[95],"Su documentación sobre valores por defecto","\ndice: \"When a new row is created and no values are specified for some of the\ncolumns, those columns will be filled with their respective default values.\" Un\nnull explícito cuenta como valor indicado. En el formulario, una casilla con un\nvalor preimpreso cuenta como rellena hasta que alguien escribe \"ninguno\" encima de\nlo impreso.",[10,322,323,324,329,330,333,334,336,337,340,341,346,347,349],{},"supabase-js escribe \"ninguno\" encima en un sitio que no esperarías: una inserción\nmúltiple, en la que pasas un array de filas. Nombra todas las columnas que\naparecen en cualquiera de las filas, y\n",[91,325,328],{"href":326,"rel":327},"https:\u002F\u002Fgithub.com\u002Fsupabase\u002Fsupabase-js\u002Fblob\u002Fmaster\u002Fpackages\u002Fcore\u002Fpostgrest-js\u002Fsrc\u002FPostgrestQueryBuilder.ts",[95],"su código fuente","\ndocumenta la opción que decide qué pasa con los huecos, ",[21,331,332],{},"defaultToNull",": \"Make\nmissing fields default to ",[21,335,127],{},". Otherwise, use the default value for the\ncolumn. Only applies for bulk inserts.\" Vale ",[21,338,339],{},"true"," mientras no la cambies.\n",[91,342,345],{"href":343,"rel":344},"https:\u002F\u002Fdocs.postgrest.org\u002Fen\u002Fstable\u002Freferences\u002Fapi\u002Fpreferences.html",[95],"La documentación de PostgREST","\ndice lo mismo para una petición que enumera sus columnas, que es lo que\nsupabase-js envía con un array: \"any missing columns in the payload will be\ninserted as ",[21,348,127],{}," value by default.\"",[10,351,352,353,356,357,359,360,363,364,366],{},"Así que aquí la segunda tarea entra con ",[21,354,355],{},"status"," vacío, aunque la tabla dé a\n",[21,358,355],{}," el valor por defecto ",[21,361,362],{},"'todo'",", y un ",[21,365,355],{}," obligatorio rechaza toda la\ninserción:",[14,368,373],{"className":369,"code":371,"language":372,"meta":23},[370],"language-js","await supabase.from('tasks').insert([\n  { title: 'Escribir el brief', status: 'doing' },\n  { title: 'Enviar la factura' },\n])\n","js",[21,374,371],{"__ignoreMap":23},[10,376,377,378,380],{},"Cualquiera de los dos arreglos sirve. Envía ",[21,379,355],{}," en cada fila, o dile a\nsupabase-js que deje en pie los valores por defecto de la tabla:",[14,382,385],{"className":383,"code":384,"language":372,"meta":23},[370],"await supabase.from('tasks').insert(rows, { defaultToNull: false })\n",[21,386,384],{"__ignoreMap":23},[10,388,389,390,393],{},"Esa opción añade la cabecera ",[21,391,392],{},"Prefer: missing=default"," que lee PostgREST. Una sola\nfila pasada como objeto, sin el array alrededor, no hace esto: una clave que falta\nen ella falta en la inserción, y se aplica el valor por defecto.",[395,396],"diagram",{"alt":397,"caption":398,"src":399},"La misma inserción de dos filas dibujada dos veces. El segundo formulario deja fuera su casilla status con asterisco. A la izquierda, con defaultToNull: true, esa casilla dice null en ámbar sobre su valor preimpreso y la inserción termina en 23502. A la derecha, con defaultToNull: false, el valor preimpreso se ve y la inserción lleva un tic.","En una inserción múltiple, una columna que una fila tiene y otra no entra como null en la que no la tiene. defaultToNull: false deja que la rellene el valor por defecto de la tabla.","\u002Fblog\u002Fnull-value-violates-not-null-constraint\u002Fnull-over-the-default-1600x720.png",[39,401,403],{"id":402},"por-qué-falla-un-upsert-con-esto-si-la-fila-ya-existe","¿Por qué falla un upsert con esto si la fila ya existe?",[10,405,406],{},"Porque Postgres revisa que la fila nueva no tenga columnas obligatorias vacías\nantes de buscar la fila existente que actualizaría. Un upsert es un formulario\nmarcado \"archívalo, o si ya hay un expediente de esta persona, cámbialo\". Postgres\nmira primero los asteriscos. Un formulario que solo trae una foto de perfil nueva\nvuelve, aunque el expediente que debía cambiar esté en el cajón.",[10,408,409,410,415,416,419],{},"El orden está escrito en\n",[91,411,414],{"href":412,"rel":413},"https:\u002F\u002Fgithub.com\u002Fpostgres\u002Fpostgres\u002Fblob\u002Fmaster\u002Fsrc\u002Fbackend\u002Fexecutor\u002FnodeModifyTable.c",[95],"el código que ejecuta una inserción",":\n\"Check the constraints of the tuple.\" va antes de \"Perform a speculative\ninsertion.\", que es el paso que encuentra el choque al que responde una cláusula\n",[21,417,418],{},"on conflict",". Tuple es la palabra de Postgres para una fila. Así que este upsert,\npensado para cambiar una columna de un perfil que existe, falla en cualquier otra\ncolumna que la tabla exija:",[14,421,424],{"className":422,"code":423,"language":372,"meta":23},[370],"await supabase.from('profiles').upsert({ id: user.id, avatar_url: url })\n",[21,425,423],{"__ignoreMap":23},[10,427,428],{},"Para cambiar algunas columnas de una fila que ya está, usa un update. Cae sobre la\nfila guardada, que tiene rellenas todas las columnas obligatorias, y cambia solo\nlo que envías:",[14,430,433],{"className":431,"code":432,"language":372,"meta":23},[370],"await supabase.from('profiles').update({ avatar_url: url }).eq('id', user.id)\n",[21,434,432],{"__ignoreMap":23},[10,436,437,438,442],{},"Deja el upsert para filas que quizá aún no existen, y envíalo con cada columna\nobligatoria.\n",[91,439,441],{"href":440},"\u002Fblog\u002Fduplicate-key-value-violates-unique-constraint","El artículo sobre claves duplicadas","\ncubre el resto de lo que necesita un upsert: con qué columna compara y qué reglas\nde lectura pide desde el navegador.",[395,444],{"alt":445,"caption":446,"src":447},"Dos carriles que empiezan con el mismo cambio de una casilla. En el carril upsert se convierte en una fila nueva cuya segunda casilla con asterisco está vacía, una barrera con asterisco se cierra en ámbar con 23502 debajo, y la barrera on conflict y el cajón de más allá nunca se alcanzan. En el carril update llega a la fila guardada con todas las casillas con asterisco rellenas, la barrera está abierta y el cajón la recibe.","La fila de un upsert se revisa en busca de columnas obligatorias vacías antes de que Postgres busque la fila que actualizaría. Un update parte de la fila guardada, que ya las tiene.","\u002Fblog\u002Fnull-value-violates-not-null-constraint\u002Fupsert-is-checked-first-1600x780.png",[39,449,451],{"id":450},"deberías-quitar-la-regla-not-null","¿Deberías quitar la regla NOT NULL?",[10,453,454],{},"Solo si la columna nunca debió ser obligatoria. Un teléfono que tu formulario trata\ncomo opcional es ese caso, y ahí quitar la regla es el arreglo correcto:",[14,456,461],{"className":457,"code":459,"language":460,"meta":23},[458],"language-sql","alter table public.profiles alter column phone drop not null;\n","sql",[21,462,459],{"__ignoreMap":23},[10,464,465,466,469,470,473],{},"Para cualquier otra columna, mantén la regla y rellena la casilla. Sin ella, cada\nfila guardada después puede quedarse sin el valor que la regla garantizaba, y tu\napp se encuentra el hueco en otro sitio: una página que lee ",[21,467,468],{},"task.category.name","\nse detiene con ",[21,471,472],{},"Cannot read properties of null",", o un recuento de tareas por\ncategoría pierde en silencio las que no tienen ninguna.",[10,475,476,477,479,480,483],{},"La columna donde quitarla sale más cara es ",[21,478,256],{},". Una fila guardada con el\ndueño vacío no se le muestra a nadie. Una regla de lectura escrita como\n",[21,481,482],{},"auth.uid() = user_id"," nunca la incluye, porque en SQL un valor vacío no es igual\na nada, ni siquiera a otro valor vacío. La tarea queda guardada, y desde entonces\nnadie la ve, tampoco la persona que la creó.",[10,485,486,487,489],{},"El arreglo para ",[21,488,256],{}," es un valor por defecto que pone a quien haya iniciado\nsesión:",[14,491,494],{"className":492,"code":493,"language":460,"meta":23},[458],"alter table public.tasks\n  alter column user_id set default auth.uid();\n",[21,495,493],{"__ignoreMap":23},[10,497,498,499,503,504,507,508,513],{},"Postgres evalúa un valor por defecto \"whenever the default value is inserted (not\nwhen the table is created)\", en\n",[91,500,502],{"href":317,"rel":501},[95],"sus propias palabras",",\nasí que cada fila nueva recibe el id de la persona que guarda. Sin nadie con\nsesión iniciada, ",[21,505,506],{},"auth.uid()"," devuelve null, como advierte\n",[91,509,512],{"href":510,"rel":511},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdatabase\u002Fpostgres\u002Frow-level-security",[95],"la guía de Supabase sobre row level security",",\ny el guardado sigue fallando. Es la regla haciendo su trabajo. El valor por\ndefecto rellena la casilla solo cuando tu app la omite, así que lo que impide que\nalguien envíe el id de otra persona sigue siendo tu regla de inserción.",[10,515,516,517,521,522,524,525,527,528,530,531,535,536,89,538,540,541,543],{},"Esa regla de inserción también cambia el mensaje que ves. Postgres ejecuta las\ncomprobaciones de Row Level Security de una tabla sobre una fila nueva antes que\nsus comprobaciones not-null:\n",[91,518,520],{"href":412,"rel":519},[95],"el mismo código de inserción","\npone \"Check any RLS WITH CHECK policies.\" antes de \"Check the constraints of the\ntuple.\". Cuando tu regla de inserción compara ",[21,523,256],{}," con ",[21,526,506],{},", un\n",[21,529,256],{}," vacío falla primero en la regla y vuelve como\n",[91,532,534],{"href":533},"\u002Fblog\u002Fnew-row-violates-row-level-security-policy","new row violates row-level security policy",".\nUn ",[21,537,82],{},[21,539,256],{}," desde tu app significa una de tres cosas: la regla de\ninserción nunca mira ",[21,542,256],{},", las reglas están desactivadas en esa tabla, o la\npetición vino de código de servidor que se las salta.",[39,545,547],{"id":546},"es-seguro-mostrar-este-error-a-tus-usuarios","¿Es seguro mostrar este error a tus usuarios?",[10,549,550],{},"No. Muestra una frase tuya y guarda el error en bruto en tus registros, porque la\nlínea de detalle, cuando Postgres la incluye, es una copia de la fila entera.",[10,552,553,554,558,559,561],{},"La línea de detalle de una clave duplicada repite un valor. Esta repite el\nformulario.\n",[91,555,557],{"href":176,"rel":556},[95],"La función que la escribe","\nmuestra la fila como ",[21,560,123],{},", con cada columna que quien llama\npuede leer. En un update, es la fila tal como se habría guardado, incluidas las\ncolumnas que nadie tocó. Un formulario que Postgres devuelve puede llevar una\nfotocopia de sí mismo grapada al rechazo, y la copia muestra todas las casillas.",[10,563,564,565,568],{},"Postgres decide quién recibe la fotocopia, y su código comprueba primero: \"Check\nif RLS is enabled and should be active for the relation; if so, then don't return\nanything.\" Una petición hecha con tu clave pública a una tabla con Row Level\nSecurity activada recibe ",[21,566,567],{},"\"details\": null",", que es lo que mostraba la consola al\nprincipio de esta página.",[10,570,571],{},"Dos llamadores reciben la copia completa.",[10,573,574,577,578,581,582,585,586,590,591,595],{},[190,575,576],{},"Un navegador, cuando las reglas de la tabla están desactivadas."," Si la consola\nmuestra ",[21,579,580],{},"Failing row contains"," para una petición que tu app hizo con la clave\npública, la Row Level Security está desactivada en esa tabla. Es la misma lectura\nque hace\n",[91,583,584],{"href":440},"el artículo sobre claves duplicadas","\nde su propia línea de detalle, y\n",[91,587,589],{"href":588},"\u002Fblog\u002Fenable-rls-on-every-supabase-table","activar las reglas en todas las tablas","\nexplica el interruptor y la primera regla. Una tabla con las reglas desactivadas\nle da sus filas a un desconocido con la misma facilidad que a ti. Nuestro\n",[91,592,594],{"href":593},"\u002Fsecurity-scanner","escaneo gratuito"," lee tu app en producción con la clave\npública que recibe cualquier visitante y lista las tablas que devuelven filas. No\nnecesita cuenta.",[10,597,598,601,602,605,606,609,610,613],{},[190,599,600],{},"Código de servidor que tiene la clave secreta."," La guía de Supabase dice que la\nclave secreta \"authorizes access through the ",[21,603,604],{},"service_role"," Postgres role, which\nhas the ",[21,607,608],{},"bypassrls"," attribute\", así que las reglas nunca se aplican y la línea de\ndetalle siempre viene rellena. Una Edge Function que actualiza una fila y responde\ncon el error que recibió, ",[21,611,612],{},"return new Response(JSON.stringify(error))",", le envía\nal navegador todas las columnas de la fila que estaba cambiando, sea de quien sea\nesa fila.",[10,615,616],{},"En el código de servidor, guarda el error y responde con tus propias palabras:",[14,618,621],{"className":619,"code":620,"language":372,"meta":23},[370],"if (error) {\n  console.error(error)\n  return new Response(JSON.stringify({ message: 'No se pudo guardar.' }), {\n    status: 400,\n  })\n}\n",[21,622,620],{"__ignoreMap":23},[10,624,625],{},"En la app, captura el código y di qué puede hacer la persona:",[14,627,630],{"className":628,"code":629,"language":372,"meta":23},[370],"const { error } = await supabase.from('tasks').insert(task)\n\nif (error?.code === '23502') {\n  setMessage('Elige una categoría antes de guardar.')\n} else if (error) {\n  setMessage('Algo salió mal. Inténtalo de nuevo.')\n}\n",[21,631,629],{"__ignoreMap":23},[10,633,634,638],{},[91,635,637],{"href":636},"\u002Fblog\u002Fmove-a-secret-to-an-edge-function","Mover una clave secreta a una Edge Function","\ncubre la parte de la función, si la clave secreta sigue en tu app.",[395,640],{"alt":641,"caption":642,"src":643},"La misma fila rechazada enviada dos veces al mismo cajón. A la izquierda, desde una ventana del navegador, el cajón lleva un candado y la respuesta dice code 23502, 400 y details null. A la derecha, desde una función que tiene una llave, el cajón no tiene candado y la línea details de la respuesta, Failing row contains, está en ámbar, con una fila de barras ámbar debajo, una por columna.","Con Row Level Security activada, el navegador no recibe copia de la fila. El código de servidor con la clave secreta la recibe entera, y la pasa si devuelve el error tal como llegó.","\u002Fblog\u002Fnull-value-violates-not-null-constraint\u002Fthe-whole-row-comes-back-1600x760.png",[39,645,647],{"id":646},"antes-de-cambiar-las-reglas-de-una-columna","Antes de cambiar las reglas de una columna",[10,649,650],{},"Guarda una copia de la base de datos de antes de añadir un valor por defecto,\nquitar una regla o rellenar un valor en todas las filas, y entérate cuando una\ntabla empiece a responder a desconocidos.",[10,652,653],{},[190,654,655,659],{},[91,656,658],{"href":657},"\u002Fsupabase-backups","Reeve Care"," guarda una copia de tu base de datos de Supabase:",[661,662,663,667,670,673,676,679],"ul",{},[664,665,666],"li",{},"una copia cifrada según un calendario, en el almacenamiento propio de Reeve en la UE, fuera de tu cuenta de Supabase",[664,668,669],{},"cada fila tal como estaba antes de que un update sobre toda la tabla la reescribiera",[664,671,672],{},"cada copia verificada antes de contar, contando las filas de cada tabla",[664,674,675],{},"también tus archivos subidos, en cuanto conectas una credencial de Storage",[664,677,678],{},"una restauración con un clic, con una copia de seguridad del estado actual tomada antes",[664,680,681],{},"todo lo que hace Monitor",[10,683,684],{},[190,685,686,690],{},[91,687,689],{"href":688},"\u002Fwebsite-monitoring","Reeve Monitor"," sigue comprobando desde fuera:",[661,692,693,696,699,702],{},[664,694,695],{},"si tu app está en línea, comprobado cada minuto",[664,697,698],{},"la comprobación de seguridad completa cada hora, en hasta tres apps",[664,700,701],{},"un correo el día en que un nuevo escaneo califica tu app peor que el anterior, para que una tabla cuyas reglas se desactivaron te llegue sin que tengas que ir a mirar",[664,703,704],{},"un informe mensual, en lenguaje claro, de lo que vio",[39,706,708],{"id":707},"qué-hacer-ahora","Qué hacer ahora",[710,711,712],"key-takeaways",{},[661,713,714,717,723,730,737,745],{},[664,715,716],{},"Lee la columna y la tabla del mensaje. Esa es la casilla que llegó vacía.",[664,718,719,720,722],{},"Comprueba si tu app omite ese campo, lo envía como ",[21,721,127],{}," o espera un valor por defecto que la columna no tiene.",[664,724,725,726,729],{},"Para cambiar algunas columnas de una fila que existe, usa ",[21,727,728],{},".update()"," con un filtro. Un upsert tiene que llevar cada columna obligatoria.",[664,731,732,733,736],{},"En una inserción múltiple, envía cada columna en cada fila, o pasa ",[21,734,735],{},"{ defaultToNull: false }"," para que se mantengan los valores por defecto de la tabla.",[664,738,739,740,742,743,215],{},"Mantén la regla en ",[21,741,256],{}," y dale a la columna el valor por defecto ",[21,744,506],{},[664,746,747],{},"Desde código de servidor, registra el error en bruto y responde al navegador con una frase tuya.",[10,749,750,751,754],{},"Empieza por la columna del mensaje y luego por la línea de tu código que construye\nla fila. Si el arreglo cambia la propia tabla, un valor por defecto nuevo o un\nupdate sobre todas las filas,\n",[91,752,753],{"href":657},"guarda una copia de la base de datos"," de antes de ejecutarlo.",{"title":23,"searchDepth":756,"depth":756,"links":757},3,[758,760,761,762,763,764,765,766],{"id":41,"depth":759,"text":42},2,{"id":182,"depth":759,"text":183},{"id":310,"depth":759,"text":311},{"id":402,"depth":759,"text":403},{"id":450,"depth":759,"text":451},{"id":546,"depth":759,"text":547},{"id":646,"depth":759,"text":647},{"id":707,"depth":759,"text":708},"Fundamentos de seguridad","\u002Fblog\u002Fnull-value-violates-not-null-constraint\u002Fcover-1200x630.png","Un formulario de papel delante de un cajón de archivador abierto, con su segunda casilla con asterisco vacía y bordeada en ámbar.","Supabase \"null value in column violates not-null constraint\": una columna obligatoria llegó vacía. De dónde vino el null, y cómo arreglarlo.",false,"md",[774,777,780,783,786],{"q":775,"a":776},"¿Qué significa null value in column violates not-null constraint?","Significa que tu app intentó guardar una fila sin valor en una columna que siempre debe tenerlo, y Postgres rechazó la fila entera. No se escribió nada. El mensaje nombra la columna y la tabla. El código es 23502, y desde una app de Supabase llega como HTTP 400.",{"q":778,"a":779},"¿Por qué la columna es null si tiene un valor por defecto?","Porque la petición nombró la columna y la envió vacía. Postgres aplica un valor por defecto solo cuando la columna se omite. supabase-js hace exactamente esto en una inserción múltiple: una columna que una fila tiene y otra no entra como null en la que no la tiene, salvo que pases defaultToNull: false.",{"q":781,"a":782},"¿Por qué mi upsert falla con un error not-null si la fila existe?","Postgres revisa que la fila nueva no tenga columnas obligatorias vacías antes de buscar la fila existente que actualizaría. Un upsert que lleva solo las columnas que quieres cambiar falla en cualquier otra columna obligatoria. Para una fila que ya existe, usa update con un filtro.",{"q":784,"a":785},"¿Debo quitar la restricción NOT NULL?","Solo si la columna nunca debió ser obligatoria, como un teléfono que tu formulario trata como opcional. Si no, el error se va y empiezan a llegar filas sin el valor, que tu app encuentra en otro sitio. Una fila con user_id vacío no se le muestra a nadie. Busca de dónde viene el null, o dale a la columna un valor por defecto como auth.uid().",{"q":787,"a":788},"¿Es seguro mostrar este error a los usuarios?","No. Cuando Postgres incluye la línea de detalle, es una copia de la fila entera. Una petición del navegador a una tabla con Row Level Security activada no recibe ninguna, pero el código de servidor con la clave secreta la recibe siempre. Captura el código 23502, muestra una frase que hayas escrito tú y guarda el error en bruto en tus registros.","\u002Fblog\u002Fnull-value-violates-not-null-constraint\u002Fcard-800x500.png",[791,792,793,794,795,796,797],"null value in column violates not-null constraint","error not null supabase","postgres 23502","failing row contains","supabase upsert not null","null value in column user_id violates not-null constraint","supabase defaulttonull",{},true,"\u002Fblog\u002Fnull-value-violates-not-null-constraint","2026-11-06",{"title":5,"description":770},"blog\u002Fnull-value-violates-not-null-constraint",[805,806,807],"Supabase \"null value in column violates not-null constraint\" (código de Postgres 23502, HTTP 400 en tu app) significa que una fila llegó sin nada en una columna que siempre debe tener un valor. El mensaje nombra la columna y la tabla. No se guardó nada.","El valor vacío viene de un campo que tu app nunca envía, de un null que tu código envía a propósito o de una columna sin el valor por defecto que dabas por hecho. Un upsert y una inserción múltiple pueden producirlo en una fila que parece completa.","Mantén la regla y arregla la fila. Desde código de servidor, nunca devuelvas el error en bruto al navegador: su línea \"Failing row contains\" puede mostrar la fila entera.","V0Ma3CDk7niTZ9O_VD4iOGxEEI6QnGQMu7q9QPWoVPo",[810,816,822,828,829,835,841,847,853,859,865,871,877,883,889,895,901,906,912,918,923,929,935,941,947,953,959,964,970,976,982,988,994,1000,1006,1012,1017,1023,1029,1035,1041,1047,1053,1059,1065,1071,1076,1082,1088,1094,1100,1106,1112,1118,1124,1130,1136,1142,1148,1154,1160,1166,1171,1177,1183,1189,1195,1201,1207,1213,1219,1225,1231,1237,1243,1249,1254,1260,1266,1272,1278,1284,1290,1296,1301,1307,1313,1319,1324,1330],{"path":811,"title":812,"description":813,"published":814,"category":767,"image":815,"draft":771},"\u002Fblog\u002Fmax-client-connections-reached","Supabase \"max client connections reached\", explicado","Supabase \"max client connections reached\": el pooler no tiene sitio para un cliente más. El puerto de tu cadena de conexión decide el límite.","2026-11-09","\u002Fblog\u002Fmax-client-connections-reached\u002Fcard-800x500.png",{"path":817,"title":818,"description":819,"published":820,"category":767,"image":821,"draft":771},"\u002Fblog\u002Funsupported-provider-provider-is-not-enabled","Error de Supabase: \"Unsupported provider: provider is not enabled\"","\"Unsupported provider: provider is not enabled\": Google está desactivado en tu proyecto Supabase. Cómo activarlo y arreglar redirect_uri_mismatch.","2026-11-08","\u002Fblog\u002Funsupported-provider-provider-is-not-enabled\u002Fcard-800x500.png",{"path":823,"title":824,"description":825,"published":826,"category":767,"image":827,"draft":771},"\u002Fblog\u002Fviolates-foreign-key-constraint","Error de Supabase \"violates foreign key constraint\" al insertar","Supabase \"insert or update on table violates foreign key constraint\": tu fila apunta a una fila que no existe. Cuál, por qué, y cómo arreglarlo.","2026-11-07","\u002Fblog\u002Fviolates-foreign-key-constraint\u002Fcard-800x500.png",{"path":800,"title":5,"description":770,"published":801,"category":767,"image":789,"draft":771},{"path":830,"title":831,"description":832,"published":833,"category":767,"image":834,"draft":771},"\u002Fblog\u002Fauth-session-missing","Supabase \"Auth session missing!\": adónde fue la sesión","\"Auth session missing!\" es supabase-js sin encontrar ninguna sesión iniciada y sin llegar a preguntar a Supabase. Adónde fue la sesión, y el arreglo.","2026-11-05","\u002Fblog\u002Fauth-session-missing\u002Fcard-800x500.png",{"path":836,"title":837,"description":838,"published":839,"category":767,"image":840,"draft":771},"\u002Fblog\u002Finvalid-claim-missing-sub-claim","Supabase \"invalid claim: missing sub claim\": no hay nadie conectado","\"invalid claim: missing sub claim\": Supabase Auth recibió un token auténtico sin ningún usuario, la clave del proyecto en lugar de una sesión.","2026-11-04","\u002Fblog\u002Finvalid-claim-missing-sub-claim\u002Fcard-800x500.png",{"path":842,"title":843,"description":844,"published":845,"category":767,"image":846,"draft":771},"\u002Fblog\u002Ffailed-to-send-a-request-to-the-edge-function","\"Failed to send a request to the Edge Function\": la causa real","\"Failed to send a request to the Edge Function\": el navegador no recibió una respuesta legible. Su consola dice por qué y si tu función se ejecutó.","2026-11-03","\u002Fblog\u002Ffailed-to-send-a-request-to-the-edge-function\u002Fcard-800x500.png",{"path":848,"title":849,"description":850,"published":851,"category":767,"image":852,"draft":771},"\u002Fblog\u002Fsupabase-auth-redirects-to-localhost","Por qué los enlaces de email de Supabase redirigen a localhost:3000","Si Supabase redirige a localhost:3000, la Site URL nunca se cambió o tu dirección falta en Redirect URLs. La solución, y el comodín que debes evitar.","2026-11-02","\u002Fblog\u002Fsupabase-auth-redirects-to-localhost\u002Fcard-800x500.png",{"path":854,"title":855,"description":856,"published":857,"category":767,"image":858,"draft":771},"\u002Fblog\u002Femail-link-is-invalid-or-has-expired","Supabase \"Email link is invalid or has expired\" con un enlace nuevo","Con un enlace enviado hace un minuto, \"Email link is invalid or has expired\" en Supabase suele indicar que un filtro de correo lo usó antes. Cómo arreglarlo.","2026-11-01","\u002Fblog\u002Femail-link-is-invalid-or-has-expired\u002Fcard-800x500.png",{"path":860,"title":861,"description":862,"published":863,"category":767,"image":864,"draft":771},"\u002Fblog\u002Fcanceling-statement-due-to-statement-timeout","Supabase \"canceling statement due to statement timeout\", explicado","Supabase \"canceling statement due to statement timeout\": una consulta superó el límite de su rol, 3 segundos para un visitante. Revisa antes la política RLS.","2026-10-31","\u002Fblog\u002Fcanceling-statement-due-to-statement-timeout\u002Fcard-800x500.png",{"path":305,"title":866,"description":867,"published":868,"category":869,"image":870,"draft":771},"\"Database error deleting user\" en Supabase, y qué se va con él","\"Database error deleting user\" en Supabase: tu base de datos rechazó el borrado. La causa está en tus logs y el arreglo decide qué más se borra.","2026-10-30","Backups","\u002Fblog\u002Fdatabase-error-deleting-user\u002Fcard-800x500.png",{"path":872,"title":873,"description":874,"published":875,"category":767,"image":876,"draft":771},"\u002Fblog\u002Fstripe-webhook-no-signatures-found","\"No signatures found matching the expected signature\" en Supabase","¿Tu webhook de Stripe en una Edge Function de Supabase dice \"No signatures found matching the expected signature\"? Tiene dos causas. Sigue verificando.","2026-10-29","\u002Fblog\u002Fstripe-webhook-no-signatures-found\u002Fcard-800x500.png",{"path":878,"title":879,"description":880,"published":881,"category":767,"image":882,"draft":771},"\u002Fblog\u002Fsupabase-invalid-login","\"Invalid login credentials\" en Supabase: las tres causas","Supabase dice \"Invalid login credentials\" ante una contraseña errónea, un email desconocido y una cuenta de Google. Cómo saber cuál es.","2026-10-28","\u002Fblog\u002Fsupabase-invalid-login\u002Fcard-800x500.png",{"path":884,"title":885,"description":886,"published":887,"category":767,"image":888,"draft":771},"\u002Fblog\u002Fedge-function-returned-a-non-2xx-status-code","\"Edge Function returned a non-2xx status code\": el error real","\"Edge Function returned a non-2xx status code\" significa que tu función de Supabase respondió con un error. Aquí ves dónde leer el real.","2026-10-27","\u002Fblog\u002Fedge-function-returned-a-non-2xx-status-code\u002Fcard-800x500.png",{"path":890,"title":891,"description":892,"published":893,"category":767,"image":894,"draft":771},"\u002Fblog\u002Fcertificate-not-trusted","Certificado no confiable: tres causas aparte de la caducidad","¿Certificado no confiable aunque la fecha está bien? Nombra otra dirección, se firmó a sí mismo o a su cadena le falta un eslabón. Cómo saber cuál.","2026-10-26","\u002Fblog\u002Fcertificate-not-trusted\u002Fcard-800x500.png",{"path":896,"title":897,"description":898,"published":899,"category":767,"image":900,"draft":771},"\u002Fblog\u002Finvalid-input-syntax-for-type-uuid","Supabase \"invalid input syntax for type uuid\": lee las comillas","Supabase \"invalid input syntax for type uuid\" significa que tu app envió algo que no es un id. Lee las comillas, frena la consulta, conserva la columna.","2026-10-25","\u002Fblog\u002Finvalid-input-syntax-for-type-uuid\u002Fcard-800x500.png",{"path":440,"title":902,"description":903,"published":904,"category":767,"image":905,"draft":771},"Supabase \"duplicate key value violates unique constraint\", explicado","Supabase \"duplicate key value violates unique constraint\": el valor ya está ocupado. Encuentra la columna, elige un arreglo y aparta el valor de la pantalla.","2026-10-24","\u002Fblog\u002Fduplicate-key-value-violates-unique-constraint\u002Fcard-800x500.png",{"path":907,"title":908,"description":909,"published":910,"category":767,"image":911,"draft":771},"\u002Fblog\u002Fsupabase-relation-does-not-exist","Supabase \"relation does not exist\" aunque la tabla está ahí","Supabase \"relation does not exist\" o PGRST205: ninguna tabla tiene ese nombre en ese esquema. Cuatro comprobaciones, y lo que abre exponer un esquema.","2026-10-23","\u002Fblog\u002Fsupabase-relation-does-not-exist\u002Fcard-800x500.png",{"path":913,"title":914,"description":915,"published":916,"category":767,"image":917,"draft":771},"\u002Fblog\u002Fsupabase-function-not-found","Supabase PGRST202: \"Could not find the function in the schema cache\"","Supabase \"Could not find the function in the schema cache\" muestra los nombres de argumentos que envió tu app. Compáralos con la función.","2026-10-22","\u002Fblog\u002Fsupabase-function-not-found\u002Fcard-800x500.png",{"path":213,"title":919,"description":920,"published":921,"category":767,"image":922,"draft":771},"Supabase PGRST204: \"Could not find the column in the schema cache\"","Supabase \"Could not find the column in the schema cache\" suele ser un nombre que no coincide. Cuatro comprobaciones y la línea que recarga.","2026-10-21","\u002Fblog\u002Fsupabase-schema-cache-column-error\u002Fcard-800x500.png",{"path":924,"title":925,"description":926,"published":927,"category":767,"image":928,"draft":771},"\u002Fblog\u002Fsupabase-email-rate-limit-exceeded","Supabase \"Email Rate Limit Exceeded\": dos límites, un solo código","Supabase \"Email Rate Limit Exceeded\" limita cada email que envía tu proyecto: 2 por hora con el remitente integrado. Qué cuenta y cómo subirlo.","2026-10-20","\u002Fblog\u002Fsupabase-email-rate-limit-exceeded\u002Fcard-800x500.png",{"path":930,"title":931,"description":932,"published":933,"category":767,"image":934,"draft":771},"\u002Fblog\u002Fsupabase-users-logged-out","Supabase \"Invalid Refresh Token: Refresh Token Not Found\": 3 causas","Supabase \"Invalid Refresh Token: Refresh Token Not Found\" cierra la sesión de tus usuarios. Tres causas, y cómo las separa el log de Auth.","2026-10-19","\u002Fblog\u002Fsupabase-users-logged-out\u002Fcard-800x500.png",{"path":936,"title":937,"description":938,"published":939,"category":767,"image":940,"draft":771},"\u002Fblog\u002Fpermission-denied-for-schema-public","Supabase \"permission denied for schema public\": tres causas","Supabase \"permission denied for schema public\" es un grant que falta en el esquema. Qué lo quitó y la línea que lo devuelve.","2026-10-18","\u002Fblog\u002Fpermission-denied-for-schema-public\u002Fcard-800x500.png",{"path":942,"title":943,"description":944,"published":945,"category":767,"image":946,"draft":771},"\u002Fblog\u002Fsupabase-bucket-not-found","Supabase \"Bucket not found\": las tres cosas que significa","Supabase \"Bucket not found\" es una de tres cosas: un enlace público a un bucket privado, un nombre que no coincide exactamente o el proyecto equivocado.","2026-10-17","\u002Fblog\u002Fsupabase-bucket-not-found\u002Fcard-800x500.png",{"path":948,"title":949,"description":950,"published":951,"category":767,"image":952,"draft":771},"\u002Fblog\u002Fsupabase-invalid-jwt","Supabase invalid JWT: ¿qué clave dejó de funcionar?","Un error Supabase invalid JWT es PGRST301: un token firmado no pasó la verificación. En un proyecto nuevo se trata de la sesión, no de la clave de API.","2026-10-16","\u002Fblog\u002Fsupabase-invalid-jwt\u002Fcard-800x500.png",{"path":954,"title":955,"description":956,"published":957,"category":767,"image":958,"draft":771},"\u002Fblog\u002Fsupabase-single-row-error","Supabase \"multiple (or no) rows returned\" casi siempre es ninguna","Supabase devuelve PGRST116, \"JSON object requested, multiple (or no) rows returned\", cuando tu consulta no encontró nada. Una fila oculta parece ausente.","2026-10-15","\u002Fblog\u002Fsupabase-single-row-error\u002Fcard-800x500.png",{"path":287,"title":960,"description":961,"published":962,"category":767,"image":963,"draft":771},"\"Database error saving new user\" en Supabase: dónde mirar","\"Database error saving new user\" significa que algo unido a tu registro de Supabase falló y se deshizo entero. El error real está en tus logs.","2026-10-14","\u002Fblog\u002Fdatabase-error-saving-new-user\u002Fcard-800x500.png",{"path":965,"title":966,"description":967,"published":968,"category":767,"image":969,"draft":771},"\u002Fblog\u002Fwhat-is-supabase","¿Qué es Supabase? La base de datos que eligió tu builder","Qué es Supabase: la base de datos, el login y el almacenamiento de archivos de tu app. Supabase lleva los servidores; las reglas sobre tus filas son tuyas.","2026-10-13","\u002Fblog\u002Fwhat-is-supabase\u002Fcard-800x500.png",{"path":971,"title":972,"description":973,"published":974,"category":767,"image":975,"draft":771},"\u002Fblog\u002Faws-access-key-exposed-in-frontend","Una clave AWS expuesta en tu frontend no es como una clave OpenAI","Una clave de acceso de AWS expuesta en tu frontend lee y escribe tu almacenamiento. La solución suele ser una presigned URL, no una clave nueva.","2026-10-12","\u002Fblog\u002Faws-access-key-exposed-in-frontend\u002Fcard-800x500.png",{"path":977,"title":978,"description":979,"published":980,"category":767,"image":981,"draft":771},"\u002Fblog\u002Fstorage-upload-violates-row-level-security","\"Row-level security policy for table objects\" al subir","\"New row violates row-level security policy for table objects\" significa que tu subida no tiene regla insert. Hacer público el bucket no añade ninguna.","2026-10-11","\u002Fblog\u002Fstorage-upload-violates-row-level-security\u002Fcard-800x500.png",{"path":983,"title":984,"description":985,"published":986,"category":767,"image":987,"draft":771},"\u002Fblog\u002Fbase44-security-scan","Escaneo de seguridad de Base44: lo que solo él ve","El escaneo de seguridad de Base44 revisa siete tipos de problema desde dentro. Esta es la mitad que lee y nadie más puede leer, y la mitad que nunca mira.","2026-10-10","\u002Fblog\u002Fbase44-security-scan\u002Fcard-800x500.png",{"path":989,"title":990,"description":991,"published":992,"category":767,"image":993,"draft":771},"\u002Fblog\u002Fapi-key-leaked-what-to-do","Tu clave API se filtró. El orden en el que hay que actuar","Una clave API se filtró y no sabes por dónde empezar. No todas las claves de tu frontend lo son, y el orden importa más que la velocidad.","2026-10-09","\u002Fblog\u002Fapi-key-leaked-what-to-do\u002Fcard-800x500.png",{"path":995,"title":996,"description":997,"published":998,"category":767,"image":999,"draft":771},"\u002Fblog\u002Finfinite-recursion-in-policy-for-relation","\"Infinite recursion detected in policy\" sin desactivar RLS","\"Infinite recursion detected in policy for relation\" significa que tu policy preguntó a la tabla que protege. Así se rompe el círculo.","2026-10-08","\u002Fblog\u002Finfinite-recursion-in-policy-for-relation\u002Fcard-800x500.png",{"path":1001,"title":1002,"description":1003,"published":1004,"category":767,"image":1005,"draft":771},"\u002Fblog\u002Fno-api-key-found-in-request","\"No API key found in request\" en Supabase, y el arreglo equivocado","\"No API key found in request\" significa que tu petición llegó a Supabase sin clave. Casi todas las respuestas apuntan a las reglas de tu base de datos.","2026-10-07","\u002Fblog\u002Fno-api-key-found-in-request\u002Fcard-800x500.png",{"path":1007,"title":1008,"description":1009,"published":1010,"category":767,"image":1011,"draft":771},"\u002Fblog\u002Fis-base44-safe","¿Es seguro Base44? Lo que vimos en 5.442 apps Base44","¿Es seguro Base44? Pasamos nueve comprobaciones a 5.442 apps Base44 en producción. Casi todas sacaron B por motivos que su dueño nunca eligió.","2026-10-06","\u002Fblog\u002Fis-base44-safe\u002Fcard-800x500.png",{"path":636,"title":1013,"description":1014,"published":1015,"category":767,"image":1016,"draft":771},"Ocultar una clave API: muévela a una Edge Function de Supabase","Ocultar una clave API significa sacarla del navegador, y una Edge Function de Supabase es el sitio más pequeño donde ponerla. Dos pasos importan más.","2026-10-05","\u002Fblog\u002Fmove-a-secret-to-an-edge-function\u002Fcard-800x500.png",{"path":1018,"title":1019,"description":1020,"published":1021,"category":767,"image":1022,"draft":771},"\u002Fblog\u002Fenv-file-exposed-on-your-server","¿Tu archivo .env está expuesto? Las doce rutas a revisar","¿Tu archivo .env está expuesto en tu propio servidor web? Doce direcciones te lo dicen, y un acierto significa que todo lo que contiene ya es público.","2026-10-04","\u002Fblog\u002Fenv-file-exposed-on-your-server\u002Fcard-800x500.png",{"path":1024,"title":1025,"description":1026,"published":1027,"category":767,"image":1028,"draft":771},"\u002Fblog\u002Fis-v0-safe","Seguridad de v0: las 1.790 aplicaciones escaneadas sacaron una A","Seguridad de v0, medida en 1.790 aplicaciones v0 en vivo: todas sacaron A. Solo 17 nombraban una base de datos, y eso es casi todo lo que mide la A.","2026-10-03","\u002Fblog\u002Fis-v0-safe\u002Fcard-800x500.png",{"path":1030,"title":1031,"description":1032,"published":1033,"category":767,"image":1034,"draft":771},"\u002Fblog\u002Fis-bolt-safe","¿Es seguro Bolt? Lo que vimos en 1.123 aplicaciones Bolt","¿Es seguro Bolt? Pasamos 1.123 aplicaciones Bolt en vivo por nueve comprobaciones. El hosting salió limpio. Los hallazgos eran claves y tablas abiertas dentro.","2026-10-02","\u002Fblog\u002Fis-bolt-safe\u002Fcard-800x500.png",{"path":1036,"title":1037,"description":1038,"published":1039,"category":767,"image":1040,"draft":771},"\u002Fblog\u002Fis-supabase-down-or-your-app","¿Supabase está caído o es tu app? Cómo distinguirlo","¿Supabase está caído o es tu app? Un proyecto pausado, una base de datos llena y un fallo tuyo parecen lo mismo. Cuatro comprobaciones los separan.","2026-10-01","\u002Fblog\u002Fis-supabase-down-or-your-app\u002Fcard-800x500.png",{"path":1042,"title":1043,"description":1044,"published":1045,"category":767,"image":1046,"draft":771},"\u002Fblog\u002Fdoes-supabase-encrypt-my-data","¿Supabase cifra mis datos? Sí. Esto es lo que detiene","¿Supabase cifra los datos? Sí: AES-256 en reposo, TLS en tránsito, SOC 2 e ISO 27001. Qué cubre cada uno, y la fuga que ninguno de ellos detiene.","2026-09-30","\u002Fblog\u002Fdoes-supabase-encrypt-my-data\u002Fcard-800x500.png",{"path":1048,"title":1049,"description":1050,"published":1051,"category":767,"image":1052,"draft":771},"\u002Fblog\u002Fsupabase-free-plan-limits","¿Cuánto cuesta Supabase? Los límites del plan gratuito","Los límites del plan gratuito de Supabase: 500 MB de base de datos, 5 GB de egress y ningún backup. Qué pasa en cada uno y qué cambia Pro por $25.","2026-09-29","\u002Fblog\u002Fsupabase-free-plan-limits\u002Fcard-800x500.png",{"path":1054,"title":1055,"description":1056,"published":1057,"category":869,"image":1058,"draft":771},"\u002Fblog\u002Ftest-your-supabase-backup","Prueba tu backup de Supabase antes del día que lo necesites","Cómo probar tu backup de Supabase: restaurarlo en un proyecto de prueba, contar filas, iniciar sesión y buscar la línea que le falta a un archivo cortado.","2026-09-28","\u002Fblog\u002Ftest-your-supabase-backup\u002Fcard-800x500.png",{"path":1060,"title":1061,"description":1062,"published":1063,"category":869,"image":1064,"draft":771},"\u002Fblog\u002Fsupabase-backup-github-action","Backup gratis de Supabase con una GitHub Action, y la trampa","Un backup de Supabase con GitHub Action es gratis y sirve a muchas apps. El workflow, el connection string correcto y el egress de cada ejecución.","2026-09-27","\u002Fblog\u002Fsupabase-backup-github-action\u002Fcard-800x500.png",{"path":1066,"title":1067,"description":1068,"published":1069,"category":869,"image":1070,"draft":771},"\u002Fblog\u002Fdownload-your-supabase-backup","Por qué no puedes descargar tu backup de Supabase","No puedes descargar tu backup de Supabase en un proyecto actual: la copia diaria es una instantánea física. Cómo comprobarlo y tener tu propia copia.","2026-09-26","\u002Fblog\u002Fdownload-your-supabase-backup\u002Fcard-800x500.png",{"path":1072,"title":1073,"description":1074,"published":1069,"category":767,"image":1075,"draft":771},"\u002Fblog\u002Fsupabase-permission-denied-for-table","Supabase \"permission denied for table\": el grant que falta","Desde el 30 de octubre, una tabla nueva de Supabase responde \"permission denied for table\" hasta que le das acceso. El grant del email es la mitad del arreglo.","\u002Fblog\u002Fsupabase-permission-denied-for-table\u002Fcard-800x500.png",{"path":1077,"title":1078,"description":1079,"published":1080,"category":767,"image":1081,"draft":771},"\u002Fblog\u002Fopen-api-endpoint-exposed","¿Un endpoint de API abierto es un problema? Mira el JSON","Tu escaneo señaló un endpoint de API abierto. Que importe depende de lo que devolvió, y la mayoría de los que encontramos eran de la plataforma.","2026-09-25","\u002Fblog\u002Fopen-api-endpoint-exposed\u002Fcard-800x500.png",{"path":1083,"title":1084,"description":1085,"published":1086,"category":767,"image":1087,"draft":771},"\u002Fblog\u002Flovable-security-scan","Escaneo de seguridad de Lovable: lo que no puede probar","Escaneo de seguridad de Lovable: qué revisan el Quick scan y el Deep scan, cuándo corre cada uno, y lo único que ningún escaneo desde dentro puede probar.","2026-09-24","\u002Fblog\u002Flovable-security-scan\u002Fcard-800x500.png",{"path":1089,"title":1090,"description":1091,"published":1092,"category":869,"image":1093,"draft":771},"\u002Fblog\u002Fsupabase-backup-auth-users","Por qué tu dump de Supabase no tiene usuarios dentro","Ejecuta supabase db dump por sí solo y obtienes la forma de tu base de datos y ninguna de sus filas, sin el esquema auth donde viven tus usuarios.","2026-09-23","\u002Fblog\u002Fsupabase-backup-auth-users\u002Fcard-800x500.png",{"path":1095,"title":1096,"description":1097,"published":1098,"category":767,"image":1099,"draft":771},"\u002Fblog\u002Fdomain-and-certificate-expiry","Dominio caducado, web caída: qué pasa a partir de ahora","Tu dominio caducó y tu web está caída. Aquí tienes el reloj que acaba de arrancar, por qué el certificado es el menor problema, y cómo comprobar ambos.","2026-09-22","\u002Fblog\u002Fdomain-and-certificate-expiry\u002Fcard-800x500.png",{"path":1101,"title":1102,"description":1103,"published":1104,"category":767,"image":1105,"draft":771},"\u002Fblog\u002Fis-lovable-safe","¿Es seguro Lovable? Lo que vimos en 18.554 aplicaciones Lovable","¿Es seguro Lovable? Pasamos 18.554 aplicaciones Lovable por nueve comprobaciones. La plataforma fue la más limpia de cinco. Los hallazgos estaban dentro.","2026-09-21","\u002Fblog\u002Fis-lovable-safe\u002Fcard-800x500.png",{"path":1107,"title":1108,"description":1109,"published":1110,"category":767,"image":1111,"draft":771},"\u002Fblog\u002Fvibe-coded-app-security-checklist","La lista de seguridad del vibe coding, en nueve comprobaciones","Una lista de seguridad del vibe coding con nueve puntos, cada uno verificable en tu app desde fuera por cualquiera, y cada uno con una prueba de una línea.","2026-09-20","\u002Fblog\u002Fvibe-coded-app-security-checklist\u002Fcard-800x500.png",{"path":1113,"title":1114,"description":1115,"published":1116,"category":767,"image":1117,"draft":771},"\u002Fblog\u002Fstripe-secret-key-in-frontend","Una clave secreta de Stripe en tu frontend puede mover dinero","Una clave secreta de Stripe expuesta en tu frontend puede reembolsar, cobrar y leer cada ficha de cliente que tienes. Tu clave pk_live_ sí debe estar ahí.","2026-09-19","\u002Fblog\u002Fstripe-secret-key-in-frontend\u002Fcard-800x500.png",{"path":1119,"title":1120,"description":1121,"published":1122,"category":767,"image":1123,"draft":771},"\u002Fblog\u002Fvite-and-next-public-env-vars","Variables de entorno de Vite expuestas: el prefijo dice publica esto","Variables de entorno de Vite expuestas en tu app: hicieron lo que pedía el prefijo. VITE_ y NEXT_PUBLIC_ significan publica esto, y la IA nunca supo el coste.","2026-09-18","\u002Fblog\u002Fvite-and-next-public-env-vars\u002Fcard-800x500.png",{"path":1125,"title":1126,"description":1127,"published":1128,"category":767,"image":1129,"draft":771},"\u002Fblog\u002Fbase44-source-maps","Seguridad en Base44: qué marca un escaneo y qué te toca arreglar a ti","Seguridad en Base44 en 5.438 apps escaneadas: tres hallazgos en casi todas son de la plataforma, el source map es la insignia de Base44 y lo tuyo es poco.","2026-09-17","\u002Fblog\u002Fbase44-source-maps\u002Fcard-800x500.png",{"path":1131,"title":1132,"description":1133,"published":1134,"category":767,"image":1135,"draft":771},"\u002Fblog\u002Fis-cursor-ai-safe","¿Es seguro Cursor AI? El editor, el código y la app que publicaste","¿Es seguro Cursor AI? Tres preguntas en una búsqueda: qué guarda Cursor, en qué falla el código que escribe y si la app que publicaste está abierta.","2026-09-16","\u002Fblog\u002Fis-cursor-ai-safe\u002Fcard-800x500.png",{"path":1137,"title":1138,"description":1139,"published":1140,"category":767,"image":1141,"draft":771},"\u002Fblog\u002Fis-replit-safe","¿Es seguro Replit? Lo que vimos en 3.042 aplicaciones Replit en vivo","¿Es seguro Replit? Pasamos 3.042 aplicaciones Replit en vivo por nueve comprobaciones externas. Los hallazgos estaban en la aplicación publicada, no en el host.","2026-09-15","\u002Fblog\u002Fis-replit-safe\u002Fcard-800x500.png",{"path":1143,"title":1144,"description":1145,"published":1146,"category":869,"image":1147,"draft":771},"\u002Fblog\u002Fsupabase-storage-backup","Supabase storage backup: tu copia de la base no tiene archivos","Un Supabase storage backup es una tarea aparte. Las copias de la base guardan la lista de tus archivos y ninguno de ellos, así que cada upload queda roto.","2026-09-14","\u002Fblog\u002Fsupabase-storage-backup\u002Fcard-800x500.png",{"path":1149,"title":1150,"description":1151,"published":1152,"category":869,"image":1153,"draft":771},"\u002Fblog\u002Fsupabase-point-in-time-recovery","Point-in-time recovery de Supabase: precio y límites","El point-in-time recovery de Supabase rebobina tu base de datos a cualquier segundo de la última semana. Cuesta $100 al mes sobre Pro y solo cubre la base.","2026-09-13","\u002Fblog\u002Fsupabase-point-in-time-recovery\u002Fcard-800x500.png",{"path":1155,"title":1156,"description":1157,"published":1158,"category":869,"image":1159,"draft":771},"\u002Fblog\u002Fsupabase-project-paused-recover","¿Proyecto de Supabase pausado? Tus datos siguen ahí","Supabase pausó tu proyecto tras una semana sin actividad. No se ha borrado nada, Restaurar está junto al nombre y tienes un año antes de que eso cambie.","2026-09-12","\u002Fblog\u002Fsupabase-project-paused-recover\u002Fcard-800x500.png",{"path":1161,"title":1162,"description":1163,"published":1164,"category":767,"image":1165,"draft":771},"\u002Fblog\u002Fsafest-ai-app-builder","¿Qué builder de apps con IA es más seguro? 30.998 apps escaneadas","¿Qué builder de apps con IA es más seguro? Escaneamos 30.998 apps activas de Lovable, Base44, Replit, v0 y Bolt. El builder no es lo que decide tu nota.","2026-09-11","\u002Fblog\u002Fsafest-ai-app-builder\u002Fcard-800x500.png",{"path":588,"title":1167,"description":1168,"published":1169,"category":767,"image":1170,"draft":771},"Activa Row Level Security en cada tabla de Supabase y compruébalo","Activar Row Level Security en Supabase sin política cierra la tabla del todo. Una política sin el interruptor no hace nada. Aquí tienes el SQL y la prueba.","2026-09-10","\u002Fblog\u002Fenable-rls-on-every-supabase-table\u002Fcard-800x500.png",{"path":1172,"title":1173,"description":1174,"published":1175,"category":767,"image":1176,"draft":771},"\u002Fblog\u002Fsupabase-rls-disabled-in-public","Supabase \"RLS disabled in public\": lo que el aviso no ve","Supabase marca \"RLS disabled in public\" como error. No dice nada de la política de lectura que deja tu tabla igual de abierta a cualquiera.","2026-09-09","\u002Fblog\u002Fsupabase-rls-disabled-in-public\u002Fcard-800x500.png",{"path":1178,"title":1179,"description":1180,"published":1181,"category":767,"image":1182,"draft":771},"\u002Fblog\u002Frotate-supabase-service-role-key","Cómo rotar una clave service_role de Supabase filtrada","Supabase dice que primero cierres la fuga. Otras guías dicen que rotes ya. Cuál es lo correcto depende de dónde se filtró tu clave service_role.","2026-09-08","\u002Fblog\u002Frotate-supabase-service-role-key\u002Fcard-800x500.png",{"path":1184,"title":1185,"description":1186,"published":1187,"category":767,"image":1188,"draft":771},"\u002Fblog\u002Fvibe-coding-security-scanners-compared","Escáneres de seguridad vibe coding comparados, el nuestro incluido","El mejor escáner de seguridad para vibe coding depende de tres preguntas que ninguna lista de funciones responde. Ocho herramientas, con precios.","2026-09-07","\u002Fblog\u002Fvibe-coding-security-scanners-compared\u002Fcard-800x500.png",{"path":1190,"title":1191,"description":1192,"published":1193,"category":767,"image":1194,"draft":771},"\u002Fblog\u002Fsupabase-security-checker","Supabase security checker: haz tú mismo las cinco comprobaciones","Un Supabase security checker lee tu app publicada en lugar de la configuración de tu proyecto. Estas son las cinco comprobaciones y cómo hacer cada una.","2026-09-06","\u002Fblog\u002Fsupabase-security-checker\u002Fcard-800x500.png",{"path":1196,"title":1197,"description":1198,"published":1199,"category":767,"image":1200,"draft":771},"\u002Fblog\u002Fvibe-coding-security-scanner","Escáner de seguridad para vibe coding: qué se le escapa","Un escáner de seguridad para vibe coding lee tu app en vivo desde fuera. Qué cubre eso, las cuatro cosas que no puede ver y cómo leer el resultado.","2026-09-05","\u002Fblog\u002Fvibe-coding-security-scanner\u002Fcard-800x500.png",{"path":1202,"title":1203,"description":1204,"published":1205,"category":767,"image":1206,"draft":771},"\u002Fblog\u002Fmissing-security-headers","Faltan cabeceras de seguridad: cuándo importa de verdad","Que falten cabeceras de seguridad es el hallazgo que más imprime nuestro escáner. Contra qué protegen y cuándo son la línea menos urgente de tu informe.","2026-09-04","\u002Fblog\u002Fmissing-security-headers\u002Fcard-800x500.png",{"path":1208,"title":1209,"description":1210,"published":1211,"category":767,"image":1212,"draft":771},"\u002Fblog\u002Fopenai-api-key-exposed-in-frontend","Tu clave de API de OpenAI está expuesta en tu frontend. Rótala.","Una clave de API de OpenAI expuesta en tu frontend no se puede atar a un dominio. Rótala hoy, mueve la llamada a tu servidor y limita el gasto.","2026-09-03","\u002Fblog\u002Fopenai-api-key-exposed-in-frontend\u002Fcard-800x500.png",{"path":1214,"title":1215,"description":1216,"published":1217,"category":767,"image":1218,"draft":771},"\u002Fblog\u002Fwhat-secrets-leak-from-vibe-coded-apps","Clave de API expuesta en el frontend: qué llevan 30.998 apps","Una clave de API expuesta en tu frontend suele ser una clave de Google Maps. Escaneamos 30.998 apps vibe-coded y contamos qué secretos se filtran de verdad.","2026-09-02","\u002Fblog\u002Fwhat-secrets-leak-from-vibe-coded-apps\u002Fcard-800x500.png",{"path":1220,"title":1221,"description":1222,"published":1223,"category":869,"image":1224,"draft":771},"\u002Fblog\u002Fsupabase-backup-tools-compared","Herramientas de backup de Supabase comparadas, la nuestra incluida","Cuatro tipos de herramienta de backup de Supabase, qué copia cada una y cuándo una GitHub Action gratuita le gana a pagar a cualquiera, nosotros incluidos.","2026-09-01","\u002Fblog\u002Fsupabase-backup-tools-compared\u002Fcard-800x500.png",{"path":1226,"title":1227,"description":1228,"published":1229,"category":767,"image":1230,"draft":771},"\u002Fblog\u002Freplit-secrets-explained","Cómo usar secrets en Replit, y qué se publica de todos modos","Cómo usar secrets en Replit: añadir uno, leerlo y arreglar las dos causas del undefined. Y las claves que la herramienta Secrets no puede mantener privadas.","2026-08-31","\u002Fblog\u002Freplit-secrets-explained\u002Fcard-800x500.png",{"path":1232,"title":1233,"description":1234,"published":1235,"category":869,"image":1236,"draft":771},"\u002Fblog\u002Fback-up-supabase-free-tier","Backups del plan gratuito de Supabase: cómo hacer uno sin terminal","No hay backups en el plan gratuito de Supabase: la copia tienes que hacerla tú. Cómo sacarla desde el panel y qué deja fuera la vía CSV.","2026-08-30","\u002Fblog\u002Fback-up-supabase-free-tier\u002Fcard-800x500.png",{"path":1238,"title":1239,"description":1240,"published":1241,"category":767,"image":1242,"draft":771},"\u002Fblog\u002Fis-supabase-secure","¿Supabase es seguro? Sí. Tu proyecto es otra pregunta","¿Supabase es seguro? La plataforma está auditada, cifrada y sometida a pentests. Su propia documentación dice dónde termina eso y empiezan tus ajustes.","2026-08-29","\u002Fblog\u002Fis-supabase-secure\u002Fcard-800x500.png",{"path":1244,"title":1245,"description":1246,"published":1247,"category":767,"image":1248,"draft":771},"\u002Fblog\u002Fwhere-to-find-supabase-api-keys","Dónde están tus claves API de Supabase: anon, service_role y URL","La URL del proyecto, la clave anon y la clave service_role están en una sola página del panel. Aquí tienes dónde está, y cuál de los cuatro va en tu app.","2026-08-28","\u002Fblog\u002Fwhere-to-find-supabase-api-keys\u002Fcard-800x500.png",{"path":533,"title":1250,"description":1251,"published":1252,"category":767,"image":1253,"draft":771},"New row violates row-level security policy en Supabase. ¿Y ahora?","\"New row violates row-level security policy\" significa que Supabase rechazó una escritura. El arreglo rápido vuelve a abrir la tabla para cualquiera.","2026-08-27","\u002Fblog\u002Fnew-row-violates-row-level-security-policy\u002Fcard-800x500.png",{"path":1255,"title":1256,"description":1257,"published":1258,"category":767,"image":1259,"draft":771},"\u002Fblog\u002Fcors-wildcard-security-risk","¿Es un comodín CORS un riesgo de seguridad? Casi nunca.","¿Es un comodín CORS un riesgo de seguridad? Casi siempre es el ajuste por defecto de tu builder y no entrega nada que tu servidor no diera ya a cualquiera.","2026-08-26","\u002Fblog\u002Fcors-wildcard-security-risk\u002Fcard-800x500.png",{"path":1261,"title":1262,"description":1263,"published":1264,"category":869,"image":1265,"draft":771},"\u002Fblog\u002Fsupabase-branching-is-not-a-backup","Supabase branching no es un backup. Solo va hacia adelante.","Por qué el branching de Supabase no es un backup: una rama arranca sin tus datos y un merge solo mueve el esquema. Para qué sirve y qué usar en su lugar.","2026-08-25","\u002Fblog\u002Fsupabase-branching-is-not-a-backup\u002Fcard-800x500.png",{"path":1267,"title":1268,"description":1269,"published":1270,"category":869,"image":1271,"draft":771},"\u002Fblog\u002Fhow-to-restore-a-supabase-backup","Cómo restaurar un backup de Supabase, y qué se rompe después","Cómo restaurar un backup de Supabase desde el panel o desde un archivo de volcado, qué reemplaza la restauración y por qué tu app puede seguir rota al terminar.","2026-08-24","\u002Fblog\u002Fhow-to-restore-a-supabase-backup\u002Fcard-800x500.png",{"path":1273,"title":1274,"description":1275,"published":1276,"category":767,"image":1277,"draft":771},"\u002Fblog\u002Fsupabase-storage-bucket-public","Tu bucket de Supabase Storage es público. ¿Es un problema?","Un bucket público de Supabase Storage significa que quien tenga la URL de un archivo puede abrirlo. No significa que alguien pueda listar lo que hay dentro.","2026-08-23","\u002Fblog\u002Fsupabase-storage-bucket-public\u002Fcard-800x500.png",{"path":1279,"title":1280,"description":1281,"published":1282,"category":767,"image":1283,"draft":771},"\u002Fblog\u002Fgoogle-api-key-exposed-in-frontend","¿Es un problema una clave de API de Google en tu frontend?","Una clave de API de Google en tu frontend es la clave que más encuentra nuestro escáner, y casi siempre no pasa nada. Un ajuste gratuito decide cuál es cuál.","2026-08-22","\u002Fblog\u002Fgoogle-api-key-exposed-in-frontend\u002Fcard-800x500.png",{"path":1285,"title":1286,"description":1287,"published":1288,"category":869,"image":1289,"draft":771},"\u002Fblog\u002Fai-agent-deleted-my-database","Un agente de IA borró mis datos de Supabase. ¿Qué puedo recuperar?","Un agente de IA borró datos de tu base. Lo que puedes recuperar ya estaba decidido antes de que se ejecutara, y los próximos minutos deciden el resto.","2026-08-21","\u002Fblog\u002Fai-agent-deleted-my-database\u002Fcard-800x500.png",{"path":1291,"title":1292,"description":1293,"published":1294,"category":767,"image":1295,"draft":771},"\u002Fblog\u002Fcan-anyone-read-your-supabase-database","¿Puede cualquiera leer tu base de datos Supabase? 3.680 apps","¿Puede cualquiera leer tu base de datos Supabase sin iniciar sesión? Escaneamos 30.998 apps activas hechas con builders de IA y medimos cuántas dicen que sí.","2026-08-18","\u002Fblog\u002Fcan-anyone-read-your-supabase-database\u002Fcard-800x500.png",{"path":1297,"title":1298,"description":1299,"published":1294,"category":767,"image":1300,"draft":771},"\u002Fblog\u002Fsource-maps-exposed-in-production","Source maps expuestos: tu app está publicando su código original","Un source map expuesto deja que cualquiera lea el código original de tu app, comentarios incluidos. La comprobación de 30 segundos y lo que de verdad importa.","\u002Fblog\u002Fsource-maps-exposed-in-production\u002Fcard-800x500.png",{"path":1302,"title":1303,"description":1304,"published":1305,"category":767,"image":1306,"draft":771},"\u002Fblog\u002Fsupabase-new-api-keys","Supabase publishable key: ¿es la misma que la anon key?","Supabase sustituyó anon y service_role por claves publicables y secretas. ¿Cuál pertenece a tu app y cuál no debe estar nunca?","2026-08-12","\u002Fblog\u002Fsupabase-new-api-keys\u002Fcard-800x500.png",{"path":1308,"title":1309,"description":1310,"published":1311,"category":869,"image":1312,"draft":771},"\u002Fblog\u002Fdoes-supabase-back-up-my-database","¿Supabase respalda mi base de datos? Depende de tu plan.","¿Supabase respalda tu base de datos? A diario en los planes de pago, y nada en el gratuito. Cómo saber cuál tienes y qué es lo que esa copia no cubre.","2026-08-11","\u002Fblog\u002Fdoes-supabase-back-up-my-database\u002Fcard-800x500.png",{"path":1314,"title":1315,"description":1316,"published":1317,"category":767,"image":1318,"draft":771},"\u002Fblog\u002Fsupabase-rls-on-but-table-still-public","Supabase Row Level Security está activado. Tu tabla sigue pública.","Activar Supabase Row Level Security no protege una tabla. Lo hacen tus políticas, y la que arregló tu app rota puede dejar entrar a cualquiera.","2026-08-10","\u002Fblog\u002Fsupabase-rls-on-but-table-still-public\u002Fcard-800x500.png",{"path":1320,"title":1321,"description":1322,"published":1317,"category":869,"image":1323,"draft":771},"\u002Fblog\u002Fversion-history-is-not-a-backup","El historial de versiones no es un backup. No recupera una tabla.","Lovable y Bolt guardan un historial de versiones de tu código. Tu base de datos es un servicio aparte, y volver atrás no te devuelve tus datos.","\u002Fblog\u002Fversion-history-is-not-a-backup\u002Fcard-800x500.png",{"path":1325,"title":1326,"description":1327,"published":1328,"category":869,"image":1329,"draft":771},"\u002Fblog\u002Fthree-ways-to-back-up-a-supabase-database","Tres formas de respaldar una base Supabase, y qué falta en cada una","El panel, pg_dump y un servicio gestionado. Qué guarda de verdad cada uno, qué deja fuera sin avisar y cuál sobrevive a perder la cuenta.","2026-08-09","\u002Fblog\u002Fthree-ways-to-back-up-a-supabase-database\u002Fcard-800x500.png",{"path":1331,"title":1332,"description":1333,"published":1328,"category":767,"image":1334,"draft":771},"\u002Fblog\u002Fwhich-api-keys-are-safe-in-your-frontend","Qué claves de API son seguras en el navegador y cuáles no","Tu clave anon de Supabase está pensada para ser pública. Tu clave service_role no, y se salta todas las reglas que pongas. Cómo distinguirlas.","\u002Fblog\u002Fwhich-api-keys-are-safe-in-your-frontend\u002Fcard-800x500.png",1794213644714]