
Paglipat’s first stack used Kotlin Multiplatform for shared business logic, Ktor for the API, Supabase for auth and data, and Next.js for the public site. Cash App, McDonald’s, and Netflix ship shared logic with a native UI. I dropped Compose Multiplatform for Web.
Compose Multiplatform for Web renders to an opaque canvas, so a public site loses SEO and accessibility. Proxying Supabase auth through Ktor fights single-use refresh tokens. The client owns auth. Ktor validates the JWT.
Is Compose Multiplatform for Web ready for production websites?
Compose Multiplatform for Web reached Beta in August 2025. Compose Web draws everything onto a single HTML5 canvas element. Search engine crawlers see only <canvas id="ComposeTarget"></canvas> with no indexable content.
The W3C notes that canvas content “is not accessible to screen readers because the content is not in the DOM and has no accessibility semantics.” Screen readers like JAWS, NVDA, and VoiceOver cannot interpret canvas-rendered content. Users cannot select or copy text. Browser DevTools become useless for layout inspection. While JetBrains added basic accessibility support in version 1.9.0-beta06, the fundamental canvas architecture makes WCAG compliance structurally difficult.
Bundle size is the other constraint. A typical Compose Web application weighs approximately 5MB including the Skia rendering engine (Skiko) at 2-3MB plus application code. Compare this to Next.js at 79-87KB base or React at 42KB. On 3G networks, 5MB means 10+ seconds of blank screen before any content appears, with no server-side rendering to provide initial HTML. Flutter Web, also canvas-based, states it is “not trying to solve accessibility” for web.
Compose Multiplatform for Web makes sense for internal tools, SaaS dashboards behind login screens, or web versions of existing mobile apps where users are already invested. Paglipat.com needed discoverability, accessibility, and a first paint. Next.js was the web UI.
How do you separate authentication from your data API in Supabase and Ktor?
The split I used: Supabase.js handles authentication on the client, while Ktor validates JWTs without managing auth state. A Ktor auth proxy has to cover JWT verification, token refresh, and session state.
The proxy approach fails because Supabase uses single-use refresh tokens with automatic rotation. When Ktor sits between client and Supabase, race conditions emerge the client may hold a stale token while the server has refreshed, causing “refresh token already used” errors. Supabase’s documentation explicitly warns that “the refresh token sent from browser to server is likely stale.” The SDK handles this automatically when authentication happens client-side.
For Ktor server JWT verification, configure the ktor-server-auth-jwt plugin to validate Supabase tokens directly. Supabase JWTs use HS256 with a shared secret by default, with the issuer at https://your-project.supabase.co/auth/v1 and audience set to authenticated. Extract user claims from the validated token sub for user ID, email for email address, role for authorization level to make access control decisions without ever managing authentication state.
install(Authentication) {
jwt("auth-supabase") {
verifier(JWT.require(Algorithm.HMAC256(jwtSecret))
.withAudience("authenticated")
.withIssuer(jwtIssuer)
.build())
validate { credential ->
if (credential.payload.getClaim("sub").asString().isNotEmpty()) {
JWTPrincipal(credential.payload)
} else null
}
}
}
Row Level Security changes which key you can use. Supabase’s service_role key completely bypasses RLS policies, granting unrestricted database access. This key should only be used for server-side webhooks, admin operations, background jobs, and migrations never for user-facing endpoints. For user-scoped operations from Ktor, either forward the user’s JWT to Supabase (maintaining RLS enforcement) or implement equivalent authorization logic in Ktor itself.
The anon key is safe for browser/mobile exposure because access is controlled by RLS policies. Supabase is transitioning to new key formats (sb_publishable_... and sb_secret_...) enabling independent rotation. Always configure CORS with specific domain whitelists rather than wildcards, including apikey and authorization in allowed headers.
KMP dependency management requires platform-aware configuration throughout
Ktor Client, Koin, kotlinx-serialization, kotlinx-datetime, and SQLDelight in a KMP project need three configuration layers: shared commonMain dependencies, platform-specific implementations via expect/actual patterns, and Gradle source set orchestration.
Ktor Client requires different engines per platform: OkHttp for Android, Darwin for iOS, CIO or Apache for JVM. Each engine has distinct capabilities HTTP/2 support varies, SSL configuration differs, WebSocket behavior changes. The expect/actual pattern lets shared code call createHttpClient() while each platform module supplies the engine. Content negotiation, logging, and timeouts add more platform branches.
Koin dependency injection for KMP uses three key patterns. First, expect/actual declarations for platform-specific dependencies like Android Context or iOS-specific APIs. Second, interface definitions in commonMain with platform implementations for testability. Third, explicit initialization from platform entry points Android’s Application class and iOS’s Swift AppDelegate must both call initKoin(). Kotlin 1.7.20+ simplified the memory model. Android Context injection still needs an explicit androidContext() setup.
kotlinx-datetime exists because Java 8 time APIs aren’t available in commonMain. The library provides cross-platform date handling but lacks built-in formatting parsing and formatting require platform-specific code using DateTimeFormatter on Android and NSDateFormatter on iOS. The community-maintained kotlinx-datetime-ext library fills gaps like YearMonth support and parsing utilities.
SQLDelight setup involves driver configuration per platform: AndroidSqliteDriver, NativeSqliteDriver for iOS, and JdbcSqliteDriver for JVM. iOS requires the -lsqlite3 linker flag. Schema files must live in commonMain. Migration files automatically determine version numbers. Windows migration verification has known issues with sqlitejdbc. Destructive migration requires custom SqlSchema implementation rather than built-in support.
Binary size adds up. Pure KMP shared code adds hundreds of KB to iOS apps. Adding Ktor and SQLDelight can push this to 6-19MB. Compose Multiplatform adds approximately 9MB for the Skia graphics library. Version catalog setup (libs.versions.toml) centralizes version management, while Gradle convention plugins reduce duplication across multi-module projects. Enable Gradle configuration cache, build cache, and parallel execution to mitigate build time impacts.
How React and Next.js line up with Compose
Jetpack Compose and React both describe the UI from the current state.
React’s useState(initialValue) mirrors Compose’s remember { mutableStateOf(initialValue) }. Both create state that triggers a UI update when it changes. React’s useMemo(compute, [deps]) corresponds to remember(keys) { compute() }. The state hoisting pattern lifting state to common ancestors, passing values down as props/parameters, and events up as callbacks works identically in both frameworks.
Side effect handling in React consolidates into useEffect what Compose splits across three functions. useEffect(() => {}, []) maps to LaunchedEffect(Unit) for mount-time effects. useEffect(() => {}, [dep]) maps to LaunchedEffect(dep) for dependency-triggered effects. useEffect(() => { return cleanup }) maps to DisposableEffect(key) { onDispose { } } for setup/cleanup patterns.
Next.js RTL support requires configuring the dir attribute on the HTML element based on locale detection, then using CSS logical properties throughout. Replace margin-left with margin-inline-start, padding-right with padding-inline-end, and text-align: left with text-align: start. Tailwind CSS 3.3+ supports logical property utilities (ms-4, ps-4, text-start) that automatically adapt to text direction. This mirrors Android’s RTL handling but requires explicit CSS architecture decisions.
Internationalization with next-intl follows patterns familiar from Android string resources. JSON message files replace XML, ICU syntax ({count, plural, =0 {None} =1 {One} other {# items}}) replaces <plurals> elements, and the t('key', { variable }) hook replaces getString(R.string.key). TypeScript augmentation provides type safety similar to Android’s generated R class.
Clean architecture in a KMP project
Cash App, Netflix, and McDonald’s use the same split: a domain layer with no platform dependencies, a data layer implementing domain interfaces, and presentation consuming domain abstractions.
The domain layer lives entirely in commonMain as pure Kotlin. It contains entity models, repository interfaces, and use case classes. Use cases follow single-responsibility patterns, accepting inputs and producing typed outputs (often sealed classes for success/error states). Flow-based use cases enable reactive data observation. This layer has zero awareness of Ktor, SQLDelight, or any platform specifics.
The data layer implements repository interfaces using Ktor Client for networking and SQLDelight for persistence. Repositories often combine remote and local data sources, fetching from network, caching locally, and falling back to cache on errors. DTOs (Data Transfer Objects) with kotlinx-serialization annotations map to domain models through explicit mapper functions, keeping serialization concerns out of domain code.
The presentation layer in shared modules can use AndroidX ViewModel (now multiplatform via lifecycle-viewmodel-compose) or custom state holders. ViewModels expose StateFlows of UI state consumed by platform views. For iOS Swift interop, SKIE automatically converts Kotlin Flows to Swift AsyncSequence and suspend functions to async functions, eliminating manual wrapper code. KMP-NativeCoroutines provides an alternative approach with explicit @NativeCoroutines annotations.
The expect/actual pattern works best for platform APIs (logging, device info, file storage) rather than business logic. The alternative pattern interfaces in commonMain with platform implementations provided via dependency injection offers better testability. Koin modules separate into commonModule for shared definitions and platformModule for platform-specific bindings.
Module organization typically splits by feature rather than layer at the top level. Each feature module contains domain, data, and presentation packages. iOS requires an umbrella module aggregating all feature modules into a single framework Kotlin/Native limits iOS apps to one Kotlin framework.
What those production apps share
Cash App has used KMP for 7+ years with 50 mobile engineers and 30 million monthly active users, sharing persistence, networking, and investing features while maintaining native UI. Netflix built their Prodicle app (supporting physical TV/film production) with shared business logic via a “lightweight Hendrix mobile SDK” specifically to support offline functionality that couldn’t move to backend. McDonald’s processes 6.5 million monthly purchases through a KMP-powered app with over 100 million downloads.
These deployments share business logic and keep a platform-native UI. Cash App explicitly states that “the vast majority of our code is written natively developer happiness and productivity is still the most important thing for us.” Forbes achieves 80%+ code sharing with this approach. Bitkey (Block’s Bitcoin wallet) reaches 95% shared code. Most teams share networking, data models, business rules, caching, validation, and analytics while keeping UI, animations, and platform integrations native.
Supabase integration through KMP works via the supabase-kt library from the community, providing KMP-compatible modules for auth, PostgREST, functions, storage, and realtime subscriptions. The library requires Ktor 3.0.0+ and uses platform-appropriate engines automatically. Testing Supabase integrations remains challenging teams use Docker Compose with TestContainers mimicking the Supabase stack (PostgreSQL + PostgREST) or mock the internal Ktor engine.
Who writes the shared module matters. The anti-pattern sees Android developers owning the shared module while iOS developers treat it as a black box. Successful implementations involve both Android and iOS engineers in shared code decisions. Cash App’s iOS engineers adapted due to Kotlin’s similarity to Swift, and their server team (also using Kotlin) now contributes to shared repositories.
Build and CI/CD complexity requires attention. iOS builds require macOS runners, adding CI cost. Building every architecture lengthens CI. Touchlab recommends building only the architecture you need. Pre-building shared frameworks as XCFrameworks reduces iOS developer rebuild time. Typical shared module build times run approximately 5 minutes for iOS framework generation.
Paglipat itself has moved on. The live site is no longer Next.js, and the consumer app is Flutter rather than KMP. The current runtime, and why Astro, Go, and Flutter replaced this stack, is in Why Paglipat Runs on Astro, Go, and Flutter.
Happy coding!
David Cruz
davthecoder.com paglipat.com

La primera pila de Paglipat usaba Kotlin Multiplatform para la lógica de negocio compartida, Ktor para la API, Supabase para la autenticación y los datos, y Next.js para el sitio público. Cash App, McDonald’s y Netflix publican lógica compartida con una interfaz nativa. Descarté Compose Multiplatform para Web.
Compose Multiplatform para Web renderiza sobre un canvas opaco, así que un sitio público pierde el SEO y la accesibilidad. Hacer de proxy de la autenticación de Supabase a través de Ktor choca con los refresh tokens de un solo uso. El cliente lleva la autenticación. Ktor valida el JWT.
¿Está listo Compose Multiplatform para Web para sitios de producción?
Compose Multiplatform para Web llegó a Beta en agosto de 2025. Compose Web dibuja todo sobre un único elemento canvas de HTML5. Los rastreadores de los buscadores solo ven <canvas id="ComposeTarget"></canvas>, sin contenido que se pueda indexar.
El W3C señala que el contenido de un canvas «no es accesible para los lectores de pantalla, porque el contenido no está en el DOM y no tiene semántica de accesibilidad». Lectores como JAWS, NVDA y VoiceOver no pueden interpretar lo que se dibuja en un canvas. Quien usa la página no puede seleccionar ni copiar texto. Las DevTools del navegador se vuelven inútiles para inspeccionar la maquetación. Aunque JetBrains añadió soporte básico de accesibilidad en la versión 1.9.0-beta06, la arquitectura de canvas de fondo hace que cumplir las WCAG sea difícil por la propia estructura.
El tamaño del bundle es la otra restricción. Una aplicación típica de Compose Web pesa aproximadamente 5 MB, motor de renderizado Skia (Skiko) incluido, de 2 a 3 MB, más el código de la aplicación. Compáralo con Next.js, de 79 a 87 KB de base, o con React, de 42 KB. En redes 3G, 5 MB son más de 10 segundos de pantalla en blanco antes de que aparezca nada, y no hay renderizado en el servidor que entregue un HTML inicial. Flutter Web, también basado en canvas, dice que «no intenta resolver la accesibilidad» en la web.
Compose Multiplatform para Web tiene sentido para herramientas internas, paneles SaaS detrás de un inicio de sesión, o versiones web de apps móviles que ya existen y en las que quien las usa ya está metido. Paglipat.com necesitaba que se pudiera encontrar, que fuera accesible y un primer pintado. Next.js fue la interfaz web.
¿Cómo separas la autenticación de tu API de datos en Supabase y Ktor?
La separación que usé: Supabase.js gestiona la autenticación en el cliente, mientras que Ktor valida los JWT sin llevar el estado de autenticación. Un proxy de autenticación en Ktor tiene que cubrir la verificación del JWT, el refresco de tokens y el estado de la sesión.
El enfoque del proxy falla porque Supabase usa refresh tokens de un solo uso, con rotación automática. Cuando Ktor se sienta entre el cliente y Supabase, aparecen condiciones de carrera: el cliente puede conservar un token ya caducado mientras el servidor ya ha refrescado, y eso provoca errores de «refresh token already used». La documentación de Supabase advierte de forma explícita de que «es probable que el refresh token enviado del navegador al servidor esté caducado». El SDK lo resuelve solo cuando la autenticación ocurre en el cliente.
Para verificar JWT en el servidor Ktor, configura el plugin ktor-server-auth-jwt de modo que valide los tokens de Supabase directamente. Los JWT de Supabase usan HS256 con un secreto compartido por defecto, con el emisor en https://your-project.supabase.co/auth/v1 y la audiencia fijada a authenticated. Saca las claims del usuario del token ya validado —sub para el identificador, email para la dirección de correo y role para el nivel de autorización— y toma las decisiones de control de acceso sin gestionar jamás el estado de autenticación.
install(Authentication) {
jwt("auth-supabase") {
verifier(JWT.require(Algorithm.HMAC256(jwtSecret))
.withAudience("authenticated")
.withIssuer(jwtIssuer)
.build())
validate { credential ->
if (credential.payload.getClaim("sub").asString().isNotEmpty()) {
JWTPrincipal(credential.payload)
} else null
}
}
}
La Row Level Security cambia qué clave puedes usar. La clave service_role de Supabase se salta por completo las políticas de RLS y concede acceso sin restricciones a la base de datos. Esa clave solo debería usarse para webhooks del lado del servidor, operaciones de administración, trabajos en segundo plano y migraciones, nunca para endpoints de cara a quien usa la app. Para operaciones acotadas a un usuario desde Ktor, o reenvías el JWT de ese usuario a Supabase (y así se sigue aplicando la RLS) o implementas en el propio Ktor una lógica de autorización equivalente.
La clave anon se puede exponer en el navegador o en el móvil sin problema, porque el acceso lo controlan las políticas de RLS. Supabase está pasando a formatos de clave nuevos (sb_publishable_... y sb_secret_...) que permiten rotarlas por separado. Configura siempre CORS con listas blancas de dominios concretos, no con comodines, e incluye apikey y authorization entre las cabeceras permitidas.
La gestión de dependencias en KMP exige configuración consciente de la plataforma de principio a fin
Ktor Client, Koin, kotlinx-serialization, kotlinx-datetime y SQLDelight en un proyecto KMP necesitan tres capas de configuración: las dependencias compartidas de commonMain, las implementaciones específicas de cada plataforma mediante patrones expect/actual, y la orquestación de los source sets en Gradle.
Ktor Client exige un motor distinto por plataforma: OkHttp en Android, Darwin en iOS, CIO o Apache en JVM. Cada motor tiene capacidades distintas. El soporte de HTTP/2 varía, la configuración de SSL no es la misma y el comportamiento de los WebSocket cambia. El patrón expect/actual deja que el código compartido llame a createHttpClient() y que cada módulo de plataforma aporte el motor. La negociación de contenido, el registro y los tiempos de espera añaden más ramas por plataforma.
La inyección de dependencias con Koin en KMP se apoya en tres patrones. El primero, declaraciones expect/actual para dependencias propias de la plataforma, como el Context de Android o API específicas de iOS. El segundo, interfaces definidas en commonMain con implementaciones de plataforma, para poder testearlas. El tercero, una inicialización explícita desde el punto de entrada de cada plataforma: la clase Application de Android y el AppDelegate de Swift en iOS tienen que llamar los dos a initKoin(). Kotlin 1.7.20 simplificó el modelo de memoria. Inyectar el Context de Android sigue necesitando un androidContext() explícito.
kotlinx-datetime existe porque las API de tiempo de Java 8 no están en commonMain. La biblioteca da un manejo de fechas que vale en todas las plataformas, pero no trae formateo de serie: parsear y formatear exige código propio de cada una, con DateTimeFormatter en Android y NSDateFormatter en iOS. La biblioteca kotlinx-datetime-ext, mantenida por la comunidad, tapa huecos como el soporte de YearMonth y utilidades de parseo.
Configurar SQLDelight implica un driver por plataforma: AndroidSqliteDriver, NativeSqliteDriver para iOS y JdbcSqliteDriver para JVM. iOS exige la bandera de enlazado -lsqlite3. Los archivos de esquema tienen que vivir en commonMain. Los archivos de migración fijan solos los números de versión. Verificar migraciones en Windows tiene problemas conocidos con sqlitejdbc. Una migración destructiva exige una implementación propia de SqlSchema, porque no hay soporte integrado para eso.
El tamaño del binario se va sumando. El código compartido de un KMP puro añade cientos de KB a las apps de iOS. Meter Ktor y SQLDelight puede empujar esa cifra hasta 6-19 MB. Compose Multiplatform suma aproximadamente 9 MB por la biblioteca gráfica Skia. El catálogo de versiones (libs.versions.toml) centraliza qué versión se usa de cada cosa, y los plugins de convención de Gradle recortan la duplicación en proyectos de varios módulos. Activa la configuration cache de Gradle, la build cache y la ejecución en paralelo para que el tiempo de compilación no se te vaya de las manos.
Cómo se alinean React y Next.js con Compose
Jetpack Compose y React describen los dos la interfaz a partir del estado de ahora.
El useState(initialValue) de React es el espejo de remember { mutableStateOf(initialValue) } en Compose. Los dos crean un estado que dispara una actualización de la interfaz cuando cambia. El useMemo(compute, [deps]) de React se corresponde con remember(keys) { compute() }. Elevar el estado —subirlo a un ancestro común, bajar los valores como props o como parámetros, y subir los eventos como callbacks— funciona igual en los dos.
Los efectos secundarios, en React, se concentran en useEffect, justo lo que Compose reparte en tres funciones. useEffect(() => {}, []) se corresponde con LaunchedEffect(Unit), para efectos en el momento de montar. useEffect(() => {}, [dep]) se corresponde con LaunchedEffect(dep), para efectos que dispara una dependencia. useEffect(() => { return cleanup }) se corresponde con DisposableEffect(key) { onDispose { } }, para el patrón de preparar y luego limpiar.
El soporte de derecha a izquierda en Next.js exige poner el atributo dir en el elemento HTML según el locale detectado, y usar después propiedades lógicas de CSS en todas partes. Sustituye margin-left por margin-inline-start, padding-right por padding-inline-end y text-align: left por text-align: start. Tailwind CSS 3.3+ trae utilidades de propiedades lógicas (ms-4, ps-4, text-start) que se adaptan solas a la dirección del texto. Se parece al RTL de Android, pero aquí las decisiones de arquitectura CSS las tienes que tomar tú, de forma explícita.
La internacionalización con next-intl sigue patrones que ya conoces de los recursos de cadenas de Android. Los archivos de mensajes en JSON sustituyen al XML, la sintaxis ICU ({count, plural, =0 {None} =1 {One} other {# items}}) sustituye a los elementos <plurals>, y el hook t('key', { variable }) sustituye a getString(R.string.key). Ampliar los tipos en TypeScript te da una seguridad parecida a la de la clase R que genera Android.
Arquitectura limpia en un proyecto KMP
Cash App, Netflix y McDonald’s usan la misma separación: una capa de dominio sin dependencias de plataforma, una capa de datos que implementa las interfaces del dominio, y una presentación que consume las abstracciones del dominio.
La capa de dominio vive entera en commonMain, en Kotlin puro. Ahí están los modelos de entidad, las interfaces de los repositorios y las clases de los casos de uso. Los casos de uso siguen la responsabilidad única: aceptan entradas y producen salidas tipadas (a menudo clases selladas, para el éxito y para el error). Los que se basan en Flow permiten observar los datos de forma reactiva. Esta capa no sabe nada de Ktor, ni de SQLDelight, ni de ningún detalle de plataforma.
La capa de datos implementa esas interfaces de repositorio con Ktor Client para la red y SQLDelight para la persistencia. Los repositorios suelen combinar una fuente remota y otra local: traen de la red, guardan en la caché local y, si hay un error, caen a esa caché. Los DTO (objetos de transferencia de datos), con anotaciones de kotlinx-serialization, se mapean a modelos de dominio con funciones de mapeo explícitas, y así la serialización no se cuela en el código de dominio.
La capa de presentación, dentro de los módulos compartidos, puede usar el ViewModel de AndroidX (ya multiplataforma a través de lifecycle-viewmodel-compose) o contenedores de estado propios. Los ViewModel exponen StateFlow con el estado de la interfaz, y las vistas de cada plataforma los consumen. Para interoperar con Swift en iOS, SKIE convierte automáticamente los Flow de Kotlin en AsyncSequence de Swift y las funciones suspend en funciones async, y te ahorra el wrapper a mano. KMP-NativeCoroutines es la alternativa, con anotaciones explícitas @NativeCoroutines.
El patrón expect/actual encaja mejor en API de plataforma (registro, información del dispositivo, almacenamiento de archivos) que en la lógica de negocio. La alternativa —interfaces en commonMain, con las implementaciones de plataforma metidas por inyección de dependencias— se deja testear mejor. Los módulos de Koin se separan en commonModule, para lo compartido, y platformModule, para los enlaces propios de cada plataforma.
Los módulos se suelen partir por funcionalidad, no por capa, en el nivel de arriba. Cada módulo de funcionalidad lleva paquetes de dominio, de datos y de presentación. iOS exige un módulo paraguas que junte todos los módulos de funcionalidad en un único framework: Kotlin/Native limita las apps de iOS a un solo framework de Kotlin.
Qué comparten esas apps en producción
Cash App lleva más de 7 años con KMP, con 50 ingenieros de desarrollo móvil y 30 millones de usuarios activos al mes: comparte persistencia, red y las funcionalidades de inversión, y mantiene la interfaz en nativo. Netflix construyó su app Prodicle (da soporte a la producción física de televisión y de cine) con lógica de negocio compartida a través de un «SDK móvil Hendrix ligero», justo para poder funcionar sin conexión, algo que no se podía mover al backend. McDonald’s procesa 6,5 millones de compras al mes con una app impulsada por KMP y más de 100 millones de descargas.
Estos despliegues comparten la lógica de negocio y dejan la interfaz nativa de cada plataforma. Cash App lo dice: «la inmensa mayoría de nuestro código está escrito en nativo; la felicidad y la productividad de quien desarrolla siguen siendo lo más importante para nosotros». Forbes pasa del 80 % de código compartido con este enfoque. Bitkey, el monedero de Bitcoin de Block, llega al 95 % de código compartido. La mayoría de los equipos comparte la red, los modelos de datos, las reglas de negocio, la caché, la validación y la analítica, y deja en nativo la interfaz, las animaciones y las integraciones con la plataforma.
Integrar Supabase desde KMP se hace con la biblioteca supabase-kt, de la comunidad, que trae módulos compatibles con KMP para autenticación, PostgREST, funciones, almacenamiento y suscripciones en tiempo real. Exige Ktor 3.0.0 o superior, y elige sola el motor que toca en cada plataforma. Testear esas integraciones sigue siendo un reto: los equipos usan Docker Compose con TestContainers que imitan la pila de Supabase (PostgreSQL + PostgREST), o hacen un mock del motor interno de Ktor.
Quién escribe el módulo compartido importa. El antipatrón es que Android lleve el módulo compartido y que iOS lo trate como una caja negra. Lo que funciona es que ingenieros de Android y de iOS participen en las decisiones del código compartido. En Cash App, quien venía de iOS se adaptó por lo cerca que está Kotlin de Swift, y el equipo de servidor (que también usa Kotlin) contribuye ahora a los repositorios compartidos.
La compilación y el CI/CD piden atención. Compilar para iOS exige runners de macOS, y eso suma coste de CI. Compilar todas las arquitecturas alarga el CI. Touchlab recomienda compilar solo la arquitectura que necesitas. Preconstruir los frameworks compartidos como XCFrameworks acorta el tiempo que tarda en recompilar quien desarrolla en iOS. Un módulo compartido típico ronda los 5 minutos para generar el framework de iOS.
Paglipat, en sí, ha seguido adelante. El sitio que está en vivo ya no es Next.js, y la app de consumo es Flutter, no KMP. El runtime de ahora, y por qué Astro, Go y Flutter sustituyeron a esta pila, está en Por qué Paglipat funciona con Astro, Go y Flutter.
¡Feliz programación!
David Cruz
davthecoder.com paglipat.com
Loading comments…Cargando comentarios…