BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Presentations Mobile Server-Driven UI at Scale

Mobile Server-Driven UI at Scale

50:15

Summary

Rafael Ring discusses the architectural evolution of server-driven UI at Nubank, moving from static mobile binaries to a sophisticated scripted framework called Catalyst. He explains how they implemented a tree-walk interpreter in Flutter to render dynamic layouts and logic from JSON payloads.

Bio

Rafael Ring is a Senior Staff Software Engineer on the Mobile Platform team at Nubank, responsible for the whole infrastructure that powers Nubank's mobile applications. Working full stack for more than a decade, he enjoys dealing with complex projects that span across the whole tech stack and is always searching for ways to innovate how we build tech.

About the conference

Software is changing the world. QCon London empowers software development by facilitating the spread of knowledge and innovation in the developer community. A practitioner-driven conference, QCon is designed for technical team leads, architects, engineering directors, and project managers who influence innovation in their teams.

Transcript

Rafael Ring: My name is Rafael. I'm a software engineer at the mobile platform team at Nubank. Before we go into the server-driven UI part, I'm just going to bring a little bit of context about Nubank. Nubank is one of the largest digital banks, 100% digital banks in the world. We're operating in Latin America, currently in Brazil, Mexico, and Colombia. We have more than 115 million customers. We serve a multiple range of products, from bank accounts, credit cards, debit cards, insurance, mobile phone services. The mobile platform team is responsible for taking care of the infrastructure for our mobile application. We have more than 40 million daily users on our app. We have peaks of over 2,000, 3,000 app opens per second around the busy times. We deal with a lot of different devices, mostly Android devices on our user base. We have more than 20,000 different device types that are being used. The mobile platform team, it serves more than 3,000 engineers at our company.

At Nubank, we try to work full stack across teams. Business teams, like product teams, they work on the mobile features directly. We're supporting more than 3,000 engineers. We work with Flutter, iOS, and Android codebases. iOS and Android are legacy, the native ones. We're migrating to Flutter, mostly where we're working right now. We serve all of the infrastructure, like frameworks to build UI, HTTP clients, storage, databases, whatever the teams need to build their features, we're providing support for them.

Mobile Development Lifecycle

I just want to go over a little bit about the mobile development lifecycle. I'm just going to give an overview on how developing a feature in mobile works. The first step of the workflow would be to just create your code, implement your feature, get it ready. As soon as your feature is ready, it's merged, it's going to be packaged into a single binary. Because as we're dealing with a mobile app, we require to have like a single compiled binary that is going to be later sent to users. We then take this binary and upload it to the app stores.

For instance, the app store for iOS, the Play Store on Google, and whatever other stores we're serving. Then there's a review process that happens for mobile apps. These stores, they have a checklist and some things that they want to check on the application to ensure that it meets the quality standards for those stores. After the review, the update will be available for users and the users will be able to download the new application version and use the feature that we just created. It looks like this. We start from the code and then we have to go through several steps until a person, a customer is able to actually use the feature that we developed.

What are the main pains that we go over on this workflow? The first one is having to work on a single package. Having to put all of the code in a single package means that I have my team, I work on my feature. I'll contribute to a single repository or even if it is split across multiple repositories, I still need to, in the end, combine to a single binary. Then another team is also working on their features, contributing to that single package. Then there's another team, and then there's another team. Then there's a huge amount of people working at the same codebase, working at the same binary, and we start to have dependency conflicts, bumping is hard, managing the queue of PRs to be merged even becomes a problem. Working with a single codebase hurts a lot, especially on a large-scale company.

The other big issue with this mobile development workflow is the lead time. If we go back to that timeline on how a feature will reach a user, we'll take an X amount of time to implement our code and get it ready. Then, for instance, at Nubank, we do weekly release cycles. Every week, we create a new version of the app and submit it to the stores, which is a fast release cycle. If we take just the single week to build this package and submit, the last leg of the flow usually takes 4 to 5 weeks to reach 85% of our user base. That means that the feature that I started working and took some time to implement can take up to 5 or more weeks to reach users, to have them available for them to use. These 4 weeks to 85% is actually pretty nice, because our customers, they're very nice.

They update their apps frequently, and we don't have many problems with that. What happens? A bug appears. Or you need to change something in the future that you just developed. We need to go back to our flow and restart again. We go back to the implementation phase and have to go through all the steps and take all of this time again. If we have a bug, we're looking at at least four or more weeks to actually be able to fix the bug. This isn't much fine. This hurts a lot when you have multiple teams implementing their features, creating bugs, creating conflicts between features. To be able to deal with this complexity of the single binary and the slow lead time, that's when server-driven UI comes to the rescue.

Generations of Server-Driven UI

Our agenda for the presentation is go over what I call different generations of server-driven UI. We'll start with the first generation, which is no server-driven UI, just pure mobile development, up until the fourth generation, where we have something that is much more flexible and generic.

1st Generation - Mobile Development

Going over the first generation. The traditional way of building mobile experiences is that the layout, the implementation is 100% client side. The server will just send the data that will be used to populate the screen. Every rule about how this is displayed on the screen, what data goes into which part of the screen is 100% client side. Let's use an example. Let's say we want to build a transaction confirmation screen that has some information about like a recipient, an amount, a date, and a confirmation button that submits the transaction. The standard way would be to just have an API. This API will produce some JSON payload with transfer details, and the mobile app will consume this. This JSON data will probably look something like this. You'll have a recipient, a timestamp, and an amount.

The mobile app will just take each one of those fields and populate on the correct space on the screen. You can just then build your transaction receipt using this data provided by the server. Now a change is needed. Let's say because of regulatory changes or because of a bug or even like product and design teams realize that we can make a UX improvement to our flow. Regulatory changes, they are frequent, especially for us on the financial services. The regulator just decides that we need to do something and we need to adapt to it. Let's say we want to have a transaction ID displayed on that same screen. We will need to go to the API server, adapt this server to include this transaction ID. We'll go into our mobile code, adapt it to consume this new JSON. We'll then go to the whole flow that we've defined before. We'll go to the first step and wait 4 or 5 weeks until we get that necessary change into production. This is the flow that usually is done when we're not relying on server-driven UI, when we're just doing the standard mobile development.

2nd Generation - Basic Server-Driven

Let's move into what would be the first server-driven UI touch on top of it. First, what is server-driven UI? The idea is that instead of just sending data to the client, we can send data in a layout interpretation. We can send UI descriptions that will be consumed by the client to be able to create the layout of the screen itself. We can deploy changes live because as soon as we update our server, the client will consume it.

In the end, the server controls what is actually being displayed and how it is displayed on the mobile app. How would we do it on our example? We can have the server decide which and how many items will appear on the details screen. We can also make the server decide what's the next screen or what happens when the user clicks the confirmation button. The client just knows how to take that definition and create a UI to be displayed. Instead of having my API server just outputting a JSON that will be consumed by the client screen, we can introduce a BFF, like a Backend for Frontend service that will handle the translation between this is like business data that my API will provide. The Backend for Frontend will transform into something that is meaningful and more UI representative for the client.

For instance, in our example, we have the same JSON data that will be sent by the API, and we can just use the BFF to transform into something like this. Instead of dealing directly with a recipient an amount and a date, I just have a vector of items. I don't care what these items actually mean. I just care that they have a label, they have a value, and the client will consume and display it. It doesn't need to understand the meaning of each of those fields. I can just send an on_submit_link that is the path the client will navigate to when the user clicks the confirmation button. We'll take that same layout that we had before. We'll just map the items into actual roles in the screen. We can wire the button to navigate to the path defined on the payload.

Then, if we go back to that issue of adding a transaction ID, we can just add a transaction ID to our API. Make the BFF add a new item to the vector of items. Automatically the client will display this transaction ID. I don't need to touch my client. I can just deploy the server side and automatically my screen will appear. We can make changes very simple with just a small modification in the way we worked before. We can just add new information to the screen by just changing the server side.

We can reorder items in case we wanted to put like amount before the other items. It's just a matter of going to the BFF, changing the order of the vector. We can change the next screen that is going to be open when I navigate. We're now much more flexible than we were before. We rely much less on app releases. How can we make it more generic? How can we even improve this implementation further? The thing is, this implementation that we make, although much more flexible than having everything client side, is still coupled with a specific screen. We have a specific screen layout. We know that it is a transaction details screen. It knows that it should fetch the vector of items. It knows that it should fetch the navigation path. The client still knows a lot about the screen implementation itself. Although we can change much more from the server. This was the second approach that we can implement, that is just a really small effort change to make clients more flexible.

3rd Generation - Templated Framework

We can move into the third generation, which is what we're calling a templated framework. What if we wanted to make more drastic changes to the actual layout? Instead of just reordering items, instead of just changing the button path, we want to make big changes to the screen itself. The solution that we came up at this point in time was to use templates. What we started doing was just defining a template. We create a name for the template, a set of variables that we're going to use for this template. We populate the variables. Then on the client side, I just check what template I'm receiving when I'm downloading this payload. I just use the variables, same as before. For instance, the items thing will be the same, but now I have a title. Now I have a submit_action. I have something that is a little bit more generic. Things will work exactly as we did before, just on a more structured way, having a template. We can fill the title. We can fill the transaction items.

Then we go to the part where we need to handle behavior. On our previous implementation, the most hard to change part would be the actual behavior of the confirmation button. How can we make this behavioral implementation easier and more flexible to be changed server side? We can start defining a framework to handle actions. We can define that an action is just something that we can execute. It's a function that will be dispatched, will be executed on the client side. It doesn't need to know exactly what the function is. We can wire a button, for instance, and say, when this button is pressed, just dispatch the execute function that was passed as a variable. I don't know what it is. I don't know what the button will do. I'll just call that function. We can start implementing some actions.

For instance, a navigate action that receives a route name and will just take the navigator and push the route that it received as an argument. We can go back to that template that we had. We can create an action of the navigate type, for instance. Wiring everything together, we have something that we can put an action on that place. When the button is clicked, it's going to automatically execute that action and will cause a navigation to happen. By having this, we can start chaining those dynamic screens together. If instead of navigating to a static screen or a client side only screen, we navigate to another one of those dynamic screens. We can start adding more templates.

For instance, I can create a template for a simple screen that just shows some information. We can chain those screens together. I open a screen. The first screen just loads a template, puts the variables, navigates to another screen that will just load the template, fill the variables. We can dynamically create flows. Now we have the capability of from the server side, chaining a sequence of screens together that the app doesn't even know exists. They only live on the server side.

The other question that pops probably at the moment is, this is really good to handle read-only screens. What if we need to make screens that edit data or screens that make writes? What we ended up implementing at the time was to just create write templates, so a text_form template. We can just define variables as before, for instance a regular expression to check if the field value is ok. We can have a submit_button that when it's clicked, it's going to make a post to whatever endpoint I want. All this kind of behavior is encoded on top of that template. By using these templates, these templates that make writes, these templates that build forms, these static information templates, we got our server-driven UI framework.

This is the third generation, which internally we call Bonafont, was the name of the framework. What it actually was is just a set of templates. We can define flows by chaining these screens together. We can create a server-side DSL, some helpers on the server side to be able to define the flow and compose these screens more easily. We use actions to represent behavior, like small bits of behavior, such as navigation and self-contained actions that we need to make. We can just define and use them as behavior. What were the issues that we still have with this implementation? Mostly it was always in an almost fits stage. What do I mean by that? We had templates, for instance.

Many times, the template was almost exactly what I needed. I just needed a small variation. I needed to change something that I didn't imagine was a variable before. We needed to know ahead of time what templates and what actions I was going to need to be able to create these screens. With a good set of templates and actions, we were able to cover a lot of stuff. Many times, we were just missing that extra mile to be able to do whatever we wanted. The thing is, every time we stumble on one of those occasions, we go back to app releases, which hurt us, they're bad, they take a long time. With actions, while we could represent behavior, they're much less flexible than the UI part. Composing behavior and business logic was much harder than composing UI over those templates. It started becoming overly complex. Adding new actions, creating a set of actions, it started to grow out of control. I'll go into that in a little bit. That was the third generation that we had, what we called our templated framework.

4th Generation - Scripted Framework

We'll move on to the top of the pyramid, which is the state we're at now, which is what we're calling a scripted framework. If we go back to the previous generation issues, like the first one, the template's always missing a little bit of change or a little bit of flexibility. How can we make them more dynamic than they were before? If we take a template that, for instance, for example, before was a Details Screen that received a title, a list of items, and a list of buttons. We change that into a generic screen that instead of having to know specifically about items, buttons, and title, it just receives three UI components: one for the top, one for the body, one for the bottom. It doesn't even know what's going to be displayed inside of the template. We can just create a UI component type, which is just something that has a type and knows how to build itself into a widget. This code is Flutter specific. We don't need to care too much about the code. It's just to illustrate. We just need something that can take this data definition and build a widget to be displayed.

For instance, I can create a text widget that will just have text as a string parameter, and knows how to build a text widget with that data that was sent to it. We go to our template implementation and we kind of have just something completely generic, like the body just takes the data that is sent to it, looks at the body field, and calls a build. It doesn't know what's building, if it is a button, if it is a text widget, a row, a column, whatever. It just builds whatever it receives. We can just take an example and have defined a dynamic template. This template will select the layout that we're going to use on the client side, which is super flexible now with just a top body and bottom. We just start adding components as data. We can, for instance, on top, just pass a payload that says this is a text component and this is the text that we're going to put inside of the text component.

For the body, we can just, for instance, create a column UI component. This column UI component has a children property where we can stack things on top of each other. We can then just use a row component that will stack items on the horizontal level. We can just add those texts. We can take another text, add inside of the row that we defined. We can add the other text inside of the row. We're building this in a much more flexible way because now, instead of dealing with a large model or a large addon on the screen, we have a very granular control of the layout because we're dealing with the UI components themselves.

What can we do to expand actions? For instance, we started creating some things like an HTTP request action that is just an action that will perform an HTTP request when it's called. This action can receive other actions that are going to be triggered when the HTTP request succeeds or fails. Then we started defining, for instance, an if action that just executes other actions based on conditions. Then we created a sequence action and maybe a fetch data action. Then this set of actions started to grow into something that is more composable and more complex. We end up with something that we can just say, the value of this field is just something that's going to use an HTTP request action that will perform a get on whatever endpoint to fetch the data that will be displayed inside of this text. I can just start composing with more complex logic by using an if action that composes with an equals action that makes a request or uses a time now or whatever. We're just growing these actions into something that has the flexibility to handle the behavior that I need.

The problem is that when we get to the point, we start having so many types of actions, so much composability of actions that we're building a scripting language. We're not dealing with actions anymore. If we take what we define as an action and call it an expression, we're actually building a simple programming language. With a very small set of expressions, these six different types, and we didn't even need to have all of those six. They just help us a little bit. Because by having Lambdas, we could implement anything else. We can just take that expression, create an interpreter that will receive one of those expressions. The interpreter will evaluate each of those six types of expressions and output a runtime value for us. This runtime value can be a Boolean or a string, a number, whatever. This interpreter can have variables that it's going to hold to be able to deal with functions with Lambdas. We can create a very simple and very small interpreter that can define arbitrary logic for us. We can define an expression as something that receives variables and evaluates into a runtime type. This can be one of the expressions that we defined before.

For instance, if we take an if expression, we can just define that it has three expressions as parameters. One for the condition, one for the then, one for the else. The implementation of the evaluation is simple. We just evaluate the condition, see if it's a Boolean value, and depending on if it is truly or falsely, we can just evaluate either the then or the else expression. This small implementation needs to be done for every one of those six cases of expressions that we've defined before. We have a tree-walk interpreter. By building this, we have what is defined as a tree-walk interpreter, that is something that just takes an expression that actually builds into a tree because we're composing expressions and evaluates them from top to bottom.

What if at this point we can also make UI components be values? It seems very valuable to us if these expressions not only could return numbers and Booleans or strings, but they could return actual UI components. This means that I can create some logic that will do some conditional branching or whatever thing I need, and output a UI component. For instance, this can maybe be a conditional component that displays A or B, depending on some expression that I'm going to evaluate. We can just go back to the small set of expressions, add UI component as a new type of literal that can be a runtime value. We go back to our interpreter diagram. We just need to have a UI component here as a runtime value. For Flutter-specific reasons, but in whatever framework you're building, you'll probably need some kind of UI context that will be used to inflate these UI components into actual widgets on the screen. We just go back to our expression definition.

Now instead of just receiving variables, we can receive context. We can start defining our UI component value as a runtime value. This will just be something that receives some data, there will be parameters, and knows how to build a widget by receiving a context and a map of variables. To illustrate this a little bit better, we can take a text UI component that we've done before, but now we just have the build receiving the context, the variables. We take the data, and we just evaluate the text parameter, which should be an expression that evolves into a string. We're not sure. We can receive an expression that evolves into anything. We just need to check if we're building actually a string from the text parameter. We put that into a text widget from Flutter. We can just return from the build. We have something that can be called to display text.

The screen implementation itself that we have, which used to be a template, now can just be an expression that I will evaluate. I'll start with an empty variables map. I will evaluate. As I'm building a screen, I expect my evaluation result to actually be a UI component. While the expression can evaluate to anything, for our specific scenario, we want to evaluate it into a UI component. Then, if it's a UI component, I'm just going to build it and display it on the screen.

This thing that we've built is pretty similar to JSX for those that know about web, in which we can mix code and UI elements, and functions can return UI elements. Functions can manipulate UI elements. This gives a lot of flexibility to work because we don't need to separate layout and logic. We can use logic to actually build layout. There's one other question that still exists. While we could compose a lot of logic by just using those expressions and creating those scripts, how can we interact with mobile environment? In the previous example, we needed to call the navigator and navigate to another screen. In the language that we defined, we didn't have a mechanism to be able to do it directly. How could we solve this problem? We can actually use Lambdas as constants.

If we go back to our interpreter, we can define that some variables are going to be constants, and these constants can hold any value of our language. This value can be, for instance, a Lambda. If we took a look at the constants, we can have it being a map that holds a symbol, and the value would be a Lambda that will receive parameters and execute something. The apply expression, which is the expression that will actually take a lambda and execute it, it's usually something that receives a Lambda, receives a set of parameters, and executes this Lambda with the parameters provided. If instead of receiving the Lambda directly, we receive a lookup into that variable, we can use the symbol name to get a lambda that is defined as a constant and executed when we're calling the apply expression.

Let's take a look at the example we did before that is interacting with the navigator. We defined another type of runtime value. It's a mobile function value, or something that's going to run inside of the device, that is defined inside of the device and not in my scripting language. When I were to evaluate my Expression, I can just define this navigate symbol or this navigate variable as something that will receive the context variables and constants and will yield a Lambda for me that receives a path and calls the navigator push. By doing this, I'm hooking my scripting language with the client-side environment. Because I can just define Lambdas for each of the behavior that I can only execute client-side, I can define as a constant and call from my script.

The way the implementation of the apply would work in this case is that we just take a function that we're going to call, that is the first argument, we evaluate. If it is a mobile function, we're going to evaluate each of the arguments that are passed to this actual function. These should be expressions that we'll handle. We'll just dispatch this function with the evaluated arguments. What about the server-side? When we got to that point, we had something that is very flexible, but it's hard to build a JSON, for instance, that can represent this behavior. To do so, I'm going to show an example of the DSL that we built. This is Clojure.

At Nubank, we use Clojure, which is a Lisp variant. This can be built in any language. We just create some kind of DSL that can just define the layout that I want to use as expressions. We only deal with expressions. These functions will actually output bits of JSON that is going to actually be sent to the client. The actual JSON that it's going to generate will look something like this, in which the root of everything is an expression that will be a literal. Because I'm trying to build a UI, I want a UI component literal. This UI component will have a type and will have some kind of data map.

For instance, I'm going to put a scaffold, which is the root layout for a screen. I can just, inside of it, have a top parameter. This top parameter should receive an expression that we want to be another UI component that can be a text component with some text to be displayed. This way, we can go from our DSL into something that can build the whole screen from the ground up. We can just define the scaffold. We can define the text for the title. We can build the whole screen dynamically with our DSL.

What's a diagram on how the server and client interact in this scenario? The way it will work is that the client will open some screen, let's call it dynamic screen, with the name xyz. When this screen is opened, the client implementation will just go to a server that we'll call like a screen provider, hit an endpoint, /api/screens/xyz. This will return an expression JSON for me, that will be something like what we defined before. This expression JSON will then be sent to our small interpreter. This small interpreter will then return a widget tree to the screen, and the screen will populate the view with that widget tree. We can start with a single screen provider that handles all the dynamic screens client-side.

Then we can start adding more API providers to serve those screens. We can start routing different screen IDs to different providers. We can have a microservices architecture. The nice thing about this is that if different screens can be routed to different API servers to provide a payload, that means that we can assign each of those providers to a team. We go into a distributed frontend scenario, a microfrontend scenario. Now each team handles their own releases, their own lifecycles. They don't need to interact on a single binary. They don't need to interact with each other. They can just work on their closed garden. Everything will be served on the app dynamically.

At this point, we have a new server-driven UI framework that we evolved from the template implementation from before. It's something that we've called Catalyst. This framework was a very small client-side interpreter. If we took each one of those six expressions we've defined before, we implement them as we did with the if implementation, we'll end up with around 500 lines of code or less. This is something that is totally distributed. Teams can work on their server. We can have as many servers as we want serving as many screens as we want. We can create a DSL server side to create those screens and create those flows. We have a scripting language that can represent arbitrary logic and compose this logic in a JSX-like environment for rendering screens. We have something that has full flexibility. The only time I need a client release right now is when I want to define one of those constants such as navigate or when I need a new UI component. In our scenario, we have the whole design system for Nubank implemented as UI components. We can just create anything by just leveraging the design system dynamically.

What was the impact of creating this framework? We've reduced the lead time from months, from 4 or 5 weeks into a couple of minutes. We go back to our original mobile lifecycle diagram. We can skip the single binary packaging. We can skip the upload to the stores. We can skip the review process and just directly go into users' hands. We don't even need users to update their apps, as long as they open a screen, we just fetch the data, evaluate the expression, and the screen is updated. We have now a lead time that is around 20 minutes from the moment that the team decides to deploy a feature, in 20 minutes, we have customers actually opening these screens on their apps. Right now, we have 90% plus of the screens on our app being developed using this framework.

The majority of the company doesn't work with Flutter directly anymore. They just interact server-side because they don't require to deal with releases. They don't require dependency management. They can just deploy on their server and have it live for customers. We're doing hundreds of deploys of those screen providers, updating, creating A/B tests, creating experiments, variations, UI adjustments. We have more than 14,000 different screens across all geos, all implemented with this framework. We actually have thousands of contributors because the internal sourcing on the company helped this evolve. When a new design system component appears, they're contributing to the framework. When a new functionality is needed, they're contributing to the framework.

This happened organically inside, which was pretty nice because people saw the benefit of not going through those steps and having this flexibility. I have some examples of how complex we can build those UIs. These are some real examples of UIs we have on our app. They're built with this framework. We even have, for instance, a drawer on the screen so we can switch between three states, and everything is handled by our framework and can be changed live with no client-side release requirements.

What are the issues that we still have with the implementation? Now we skipped the release cycle, but we have a connection requirement. People need to be able to be online to download this payload in order to display this screen. It's something that we're handling with cache. We're caching those expressions on the client side. When the user tries to open the screen again, it has already a version of it cached. The other issue was learning curve. As we created our own scripting language and DSL, like getting people across the company to learn how this works and to be able to proficiently use this was a huge effort. We tackled by creating lots of documentation, having developer advocates, having training example materials, and we spread this throughout the company.

The other big issue right now is that for very complex screens, we can have huge payload size. If you create a very complex expression, your payload will grow quickly. These are open issues that we still need to improve. The next version we're seeing in the future would have persistent connection to be able to fetch those screens faster without requiring to open a new HTTP connection every time. We'll have Delta updates to tackle that payload problem. If we know the difference between one screen and another, we can just calculate the Delta and send only the Delta without requiring to send a whole new payload again. Nowadays we're caching the whole screen expression, but we can have more granular caching. Caching logic by function would also help reduce the payload that is going to be sent to the client.

Final Thoughts on Mobile Server-Driven UI

Some final thoughts about this. Server-driven UI, all the examples we saw from the bottom to the top, it helps to quickly adapt to changes. It makes us more flexible to change our app when something appears. It reduces the lead time and also adds an instant adoption for customers because I can just deploy my server and update the UI automatically. It makes it much easier to perform A/B testing because as things are on the server side, on a space that we control, it's much easier to experiment, to create variations, and to deploy these variations to different populations. It also helps us create a distributed architecture for the frontend, exiting from a scenario in which we had a single binary, a single workspace in which everyone would need to contribute into something that is much more wide and open.

Also, a side effect of this that was pretty useful for us is that it avoids client-side leaks because we had lots of people, for instance, decompiling the app to try to find some features that are not available yet. If the features are on the server side, they can't find by looking at the client side. It helped us a lot in the past about this. If we go back to the pyramid that we defined, we went from something that was pretty static and pretty inflexible to something that is very generic, very expansible, and very dynamic.

The thing is, you don't need to climb all the way from the bottom to that fully-fledged scripted solution. Of course, the higher you go, the more flexible you are. The scripted solution makes us able to change behavioral logic easily, quickly make big layout changes on the app. At the same time, we're decreasing the cost of changes by moving higher up. The first layer, the first implementation that is mobile only, making a change is expensive. The lead time is expensive. At the same time, there are costs that go up as I go up on this pyramid. Probably for each org size, there's a step of the way that makes sense. For a small org size, probably doesn't make sense to create a fully-fledged scripting language just to build UI. You can stop on the templated. You can stop on the first implementation. You can just vary along the way. It's much more of finding the balance on how flexible you want to be and what actually helps your business in terms of flexibility.

Questions and Answers

Participant 1: I had a question about the app review process. Given that it's a bit less predictable what the app is actually going to do, looking just at the package, did you encounter any issues getting the app approved?

Rafael Ring: We didn't have much issues with app reviews. The version that goes to review has some kind of representative view of the actual application. When we submit to review, we have some user that we provide to Apple and Google that just accesses some client-side cached versions of those expressions. They may vary a little bit, but they're pretty close to reality. Because every time we change something, on the next release, we'll catch up with these implementations. They'll see something that is representative of the actual behavior. As long as we're not doing something really crazy, which we aren't, they are fine with having a static version that we bundle later.

 

See more presentations with transcripts

 

Recorded at:

Mar 18, 2026

BT