Dos ideas cargan con casi todo el peso. Tu cuenta es una sesión de autenticación de verdad, no una fila contra la que comparamos una contraseña. Y quien decide qué puedes leer es la base de datos, no la app — así que un cliente modificado, o alguien hablando directamente con la API con la clave pública, obtiene exactamente las mismas respuestas que la app.
Cuentas: bcrypt, una sesión real y un puente a tu perfil
La identidad vive en Supabase Auth. Las contraseñas se hashean con bcrypt en el servidor; la app nunca ve un hash y no hay ninguna comparación casera en el camino de inicio de sesión. Entrar produce un JWT con tu alcance, y una columna de tu fila de perfil (auth_uid) es el único vínculo entre el usuario de autenticación y el perfil — que es lo que hace expresable cada política de más abajo.
Puedes entrar con correo y contraseña, o con Google a través del Credential Manager de Android, que entrega un token de identidad de Google real a Supabase para el intercambio. Cerrar sesión en todas partes mata la sesión globalmente.
Queda una columna de PIN heredada de antes de Supabase Auth. Nadie la lee, las cuentas nuevas se crean con ella vacía y está en cola para borrarse. Se menciona aquí solo porque existe y la encontrarías.
Passkeys: verificados en el servidor, con guardia antirrepetición
Korat admite passkeys — un par de claves creado dentro del hardware seguro de tu teléfono, desbloqueado con tu huella o tu cara, que nunca sale del dispositivo. La parte de confianza es koratland.com, atada a la app Android mediante assetlinks.json, y la verificación corre en un Worker de Cloudflare y no en una biblioteca:
- el reto es de un solo uso, con cinco minutos de vida, y la fila se borra al consumirse
- el
rpIdHashde los datos del autenticador debe ser igual al SHA-256 dekoratland.com - la bandera de presencia de la persona tiene que estar puesta
- la firma se verifica con WebCrypto — ECDSA P-256 o RS256
- el contador de firma tiene que aumentar; un contador que se repite o retrocede se rechaza como repetición
El registro exige una sesión existente y toma la identidad de quien llama de su token de portador, nunca de un identificador puesto en el cuerpo de la petición — porque si no, cualquiera podría registrar un passkey en la cuenta de cualquiera. La fila de credencial guardada no tiene ninguna política de actualización, así que ni su dueño puede reescribir su propia clave pública ni su contador de firma; renombrar un passkey pasa por una función que solo puede tocar la etiqueta.
Una omisión deliberada: la atestación no se verifica. Korat no comprueba qué fabricante hizo el autenticador, así que nada de esto debe leerse como una afirmación atestiguada por hardware.
Estado honesto. El *registro* de passkeys está verificado y funciona. El *inicio de sesión* con passkey no se ha probado de principio a fin todavía, y la app web no admite passkeys en absoluto: es una función de Android en despliegue. Los caminos en uso diario son correo y contraseña, y el acceso con Google.
Seguridad a nivel de fila: responde la base de datos, no la app
La seguridad a nivel de fila está activada en toda la base de datos. Las políticas se escriben con cuatro funciones, y casi toda regla en Korat es una de ellas:
- app_uid()
- El id de perfil de quien llama, o nulo si no hay sesión. La base de toda política de «solo tus propias filas».
- is_member_of(business_id)
- Cierto para el propietario, o para quien tenga una fila de pertenencia activa en ese negocio.
- has_capability(business_id, capability)
- Expande los roles predefinidos — propietario, encargado, camarero, cocina — en el servidor, para que la base de datos nunca se fíe de lo que el cliente crea que significa un rol.
- can_review(business_id)
- Lee la vista de elegibilidad, que convierte «solo los clientes reales pueden reseñar» en una propiedad de la base de datos y no en una promesa de la app.
Tres reglas se aprendieron lo bastante caras como para dejarlas escritas. Una política escrita solo desde el punto de vista de la tienda excluye al cliente — una sesión de mesa, una llamada al personal y una transacción tienen dos lectores legítimos, así que se leen user_id = app_uid() or is_member_of(business_id). Una tabla hija debe ser legible exactamente hasta donde lo es su tabla madre, así que comentarios y reacciones delegan en la publicación en vez de llevar su propia idea de quién puede leerlos. Y una función de definidor corre como su propietario, así que tiene que comprobar el permiso ella misma.
Lo que la seguridad a nivel de fila no puede hacer, y qué lo sustituye
La seguridad a nivel de fila puede responder *quién llama*. Nunca puede responder *si esta petición traía un token válido* — un QR de mesa, un enlace compartido, una invitación. Así que todo camino con token en Korat es una función SECURITY DEFINER que comprueba el token ella misma, y la tabla se queda cerrada.
Entrar en una mesa. Quien está a punto de escanear un QR no es ni el anfitrión de la sesión ni personal, así que no puede leer la tabla de sesiones de mesa en absoluto, ni siquiera para buscar el token. join_session_by_token compara el token en el servidor, hace anfitrión a quien escanea primero, añade a la persona y no devuelve nada cuando el token es incorrecto, en vez de un mensaje con el que se pudiera acotar una conjetura.
Enlaces compartidos. resolve_share devuelve el tipo y el destino de un enlace y nada más: nunca quién lo compartió ni cuántos clics tiene. Contar un clic pasa por record_share_click, así que cualquiera puede ser contado sin ningún permiso de inserción en la tabla de analítica; deliberadamente no hay política de inserción para nadie.
El dinero. Hacer un pedido corre entero en el servidor. Pone el precio de cada línea él mismo y lee la tarifa de reparto de la propia fila del negocio, ignorando lo que mandó el cliente — porque una tarifa que el cliente puede fijar es un descuento que el cliente puede concederse.
Contactos, dispositivos y el inicio de sesión que no hiciste tú
El código de verificación de un correo de respaldo lo genera una función que solo puede llamar el rol de servicio. Solo se guarda el SHA-256 del código, caduca en diez minutos, admite cinco intentos y está limitado a una petición por minuto. El Worker que envía el correo nunca devuelve el código a la app.
No puedes marcar tu propio contacto como verificado. Un disparador de base de datos fuerza la marca a falso en cualquier escritura desde el cliente, y solo una función de definidor que haya comprobado de verdad un código puede ponerla. Parece paranoico hasta que lo sigues hasta el final: sin eso, añadir tu dirección a la cuenta de otra persona y pulsar «he olvidado la contraseña» es una toma de control completa. Por lo mismo, un contacto tiene que estar verificado antes de poder hacerse principal: la dirección principal es a la que escribe el sistema.
Todos los dispositivos con sesión aparecen en Ajustes y se pueden revocar de uno en uno. La tabla de dispositivos no tiene ninguna política de inserción, actualización ni borrado, porque un dispositivo recién revocado todavía tiene un token válido y si no podría borrar su propia revocación. Revocar un dispositivo borra la sesión subyacente, así que el token de refresco muere con ella.
Un inicio de sesión desde un dispositivo nuevo escribe el aviso dentro de la app en la misma transacción que crea la fila del dispositivo, para que no se pueda perder, y además manda un correo — solo a contactos verificados, porque una dirección sin verificar puede pertenecer a otra persona. El correo sale como mucho una vez por dispositivo y solo para dispositivos creados en la última hora, para que el endpoint no se pueda convertir en una forma de inundar a quien es dueño de la cuenta.
El límite de la revocación, dicho sin rodeos. Un token de acceso ya emitido vive alrededor de una hora y no se puede retirar a mitad de vuelo. Un dispositivo revocado que esté sin conexión sigue funcionando hasta que su token caduca. Esa es la naturaleza de los JWT, no un fallo, y por eso existe la lista de dispositivos en lugar de una afirmación de que la revocación es instantánea.
Lo que puedes hacer con tu propia cuenta
Borrar la cuenta se hace solo, desde Ajustes: no hay que escribir a nadie. Es una *solicitud* registrada y cancelable, no un botón que pulsas a las dos de la mañana y ya no puedes deshacer. Su estado se consulta dentro de la app. privacy@koratland.com sigue ahí para los demás derechos del PDPA: acceso, rectificación, portabilidad y retirada del consentimiento.
Denunciar cubre contenidos además de personas: perfiles, publicaciones, comentarios, historias, mensajes y páginas de negocio. La función que recibe una denuncia es SECURITY INVOKER, la única así en toda la trastienda. Como definer *vería* publicaciones ocultas y privadas y respondería distinto para ellas, lo que convierte el botón de denunciar en un oráculo sobre contenido que no te está permitido ver. Ejecutada como quien llama, «no existe esa publicación» y «esa publicación no es tuya para verla» se funden en una sola respuesta — lo cual es honesto, porque desde donde está quien denuncia son la misma cosa. El límite de frecuencia vive en un trigger, no en la app.
Lo que Korat no hace
Esta lista es la razón por la que merece la pena leer el resto de la página. Todo lo de aquí es cierto hoy.
- Nada está cifrado de extremo a extremo. Los mensajes, los archivos y las llamadas están protegidos en tránsito por TLS y en reposo por seguridad a nivel de fila. El servidor puede leerlos. Korat no tiene claves en el dispositivo ni gestión de claves, y no debería elegirse para nada que las necesite.
- No hay cifrado en reposo a nivel de aplicación más allá del que aporte la base de datos gestionada.
- Nunca se ha comprobado un documento de identidad. No hay eKYC. Nada en Korat dice «identidad verificada», y las insignias de confianza dicen en su lugar qué pruebas existen.
- Los números de teléfono no se pueden verificar: no hay proveedor de SMS conectado, y el endpoint lo dice en vez de fingir.
- Los archivos subidos se pueden leer públicamente por URL. Avatares, imágenes de publicaciones e imágenes de chat viven en buckets públicos; escribir exige una sesión, pero una URL que se filtra es un archivo que se puede descargar. Todavía no hay URLs firmadas.
- Las comprobaciones de permisos de la consola de negocio corren hoy en el dispositivo, con la misma matriz disponible en el servidor como
has_capability(). Trata los permisos de la consola como un control organizativo, no como una frontera de seguridad, hasta que ese despliegue termine. - Las llamadas no tienen servidor TURN y no están cifradas de extremo a extremo; entre ciertas redes, algunas no llegarán a conectar.
- No hay copias de seguridad de la base de datos en el plan actual, ni suite de pruebas automatizadas.
- La app web no tiene notificaciones push. El push es una función de la app Android; en la web ves la notificación al abrir la pestaña, no cuando llega.
Si algo de eso cambia, esta página cambia con ello. Sería más fácil escribir un párrafo sobre cifrado de grado bancario y seguir adelante; esto es más útil. El razonamiento detrás de estas decisiones está en la página acerca de.