# Nativeblocks SDK Documentation
> Nativeblocks is a server-driven UI framework that enables developers to build and update mobile app interfaces remotely without requiring app store releases.
## Documentation
### [Introduction](https://nativeblocks.io/docs/get-started/introduction/)
Let's discover Nativeblocks in less than 5 minutes.
## What is Nativeblocks?
Nativeblocks is a code-push platform for Android and iOS teams. You write screens in Kotlin or Swift using the Nativeblocks DSL, deploy them from the CLI, and users pick up the change on their next app open, no App Store or Play Store review required.
The platform has three main parts:
- SDK: a lightweight library (under 1 MB) you drop into your existing Android or iOS project. It integrates with Jetpack Compose and SwiftUI, and works with your existing components.
- CLI: a command-line tool that initializes your project, generates a typed DSL from your registered components, and deploys frames to production.
- Dashboard: where you manage releases, monitor tag distribution, track daily active users, and roll back to any previous version with one click.
## Core concepts
### Blocks
A Block is any native UI component annotated with `@NativeBlock` (Kotlin) or the equivalent macro (Swift).
Once annotated, `nativeblocks code-gen` generates a typed DSL wrapper so you can compose it in a Frame. See [Block & Action](/docs/sdk/block-action/) for the full annotation reference.
### Actions
An Action is a reusable piece of business logic annotated with `@NativeAction`. Actions are attached to block events in a frame and run when that event fires. Multiple actions on the same event slot run top to bottom.
There is also a built-in `script` action for inline logic, reading and writing frame variables, without needing a separate class. See [Block & Action](/docs/sdk/block-action/) for details.
### Frames
A Frame is a screen defined entirely in the Nativeblocks DSL. It declares variables, optional lifecycle hooks, and a tree of Blocks wired to Actions. Frames are what you version and deploy.
### Code Push
When you run `nativeblocks frame deploy --tag v1.2.0`, the compiled frame is uploaded to the Nativeblocks cloud. The next time a user opens the app, the SDK fetches the latest active frame for each route and renders it. No rebuild, no resubmission.
You can target rollouts by user, app version, or percentage, and roll back to any previous tag instantly from the dashboard.
## Sample DSL
A complete frame combining variables, a Block, and an Action:
**Android**
```kotlin:welcome-android.kt showLineNumbers
package com.example.app.frames
import com.example.app.generated.shared.*
import com.example.app.generated.integration.android.Script.script
import com.example.app.generated.integration.android.nativeblocks.column.NativeblocksColumn
import com.example.app.generated.integration.android.nativeblocks.column.nativeblocksColumn
import com.example.app.generated.integration.android.nativeblocks.text.nativeblocksText
import com.example.app.generated.integration.android.nativeblocks.button.nativeblocksButton
fun frame(): NativeblocksFrame = androidFrame(name = "Welcome", route = "/welcome") {
val greeting by remember("Hello, world!")
val count by remember(0)
val buttonLabel by remember("Tap me")
rootBlock {
nativeblocksColumn(
blockKey = "root",
width = NativeblocksColumn.WidthOptions.Match,
horizontalAlignment = NativeblocksColumn.HorizontalAlignmentOptions.Center,
content = {
nativeblocksText(blockKey = "heading", text = greeting, fontSize = "24")
nativeblocksText(blockKey = "counter", text = count, fontSize = "16")
nativeblocksButton(
blockKey = "incrementBtn",
label = buttonLabel,
onClick = {
script { getVariable, _, _ ->
"""updateVariable("${count.key}", String(Number(${getVariable(count)}) + 1));"""
}
},
)
},
)
}
}
```
**iOS**
```kotlin:welcome-ios.kt showLineNumbers
package com.example.app.frames
import com.example.app.generated.shared.*
import com.example.app.generated.integration.ios.Script.script
import com.example.app.generated.integration.ios.nativeblocks.vstack.NativeblocksVstack
import com.example.app.generated.integration.ios.nativeblocks.vstack.nativeblocksVstack
import com.example.app.generated.integration.ios.nativeblocks.text.nativeblocksText
import com.example.app.generated.integration.ios.nativeblocks.button.nativeblocksButton
fun frame(): NativeblocksFrame = iosFrame(name = "Welcome", route = "/welcome") {
val greeting by remember("Hello, world!")
val count by remember(0)
val buttonLabel by remember("Tap me")
rootBlock {
nativeblocksVstack(
blockKey = "root",
width = NativeblocksVstack.WidthOptions.Fill,
alignmentHorizontal = NativeblocksVstack.AlignmentHorizontalOptions.Center,
content = {
nativeblocksText(blockKey = "heading", text = greeting, fontSize = "24")
nativeblocksText(blockKey = "counter", text = count, fontSize = "16")
nativeblocksButton(
blockKey = "incrementBtn",
label = buttonLabel,
onClick = {
script { getVariable, _, _ ->
"""updateVariable("${count.key}", String(Number(${getVariable(count)}) + 1));"""
}
},
)
},
)
}
}
```
**Android**
```tsx:welcome-android.tsx showLineNumbers
import { androidFrame, NativeblocksFrame, remember } from "@nativeblocks";
import { script } from "@nativeblocks/android/Script/Script";
import { NativeblocksColumn } from "@nativeblocks/android/nativeblocks/column/NativeblocksColumn";
import { NativeblocksText } from "@nativeblocks/android/nativeblocks/text/NativeblocksText";
import { NativeblocksButton } from "@nativeblocks/android/nativeblocks/button/NativeblocksButton";
function frame(): NativeblocksFrame {
const frame = androidFrame({ name: "Welcome", route: "/welcome" });
const greeting = remember("Hello, world!");
const count = remember(0);
const buttonLabel = remember("Tap me");
return frame.rootBlock(
{
updateVariable(count, String(Number(getVariable(count)) + 1));
})}
/>
);
}
```
**iOS**
```tsx:welcome-ios.tsx showLineNumbers
import { iosFrame, NativeblocksFrame, remember } from "@nativeblocks";
import { script } from "@nativeblocks/ios/Script/Script";
import { NativeblocksVstack } from "@nativeblocks/ios/nativeblocks/vstack/NativeblocksVstack";
import { NativeblocksText } from "@nativeblocks/ios/nativeblocks/text/NativeblocksText";
import { NativeblocksButton } from "@nativeblocks/ios/nativeblocks/button/NativeblocksButton";
function frame(): NativeblocksFrame {
const frame = iosFrame({ name: "Welcome", route: "/welcome" });
const greeting = remember("Hello, world!");
const count = remember(0);
const buttonLabel = remember("Tap me");
return frame.rootBlock(
{
updateVariable(count, String(Number(getVariable(count)) + 1));
})}
/>
);
}
```
**Android**
```swift:welcome-android.swift showLineNumbers
import NativeblocksANDROID
func frame() -> any NativeblocksFrame {
androidFrame(name: "Welcome", route: "/welcome") { f in
let greeting = remember("Hello, world!")
let count = remember(0)
let buttonLabel = remember("Tap me")
f.rootBlock {
NativeblocksColumn(blockKey: "root") {
NativeblocksText(blockKey: "heading", text: greeting)
.fontSize("24")
NativeblocksText(blockKey: "counter", text: count)
.fontSize("16")
NativeblocksButton(blockKey: "incrementBtn", label: buttonLabel)
.onClick {
script { getVariable, _, _ in
#"updateVariable("\#(count.key)", String(Number(\#(getVariable(count))) + 1));"#
}
}
}
.width("match")
.horizontalAlignment("center")
}
}
}
```
**iOS**
```swift:welcome-ios.swift showLineNumbers
import NativeblocksIOS
func frame() -> any NativeblocksFrame {
iosFrame(name: "Welcome", route: "/welcome") { f in
let greeting = remember("Hello, world!")
let count = remember(0)
let buttonLabel = remember("Tap me")
f.rootBlock {
NativeblocksVstack(blockKey: "root") {
NativeblocksText(blockKey: "heading", text: greeting)
.fontSize("24")
NativeblocksText(blockKey: "counter", text: count)
.fontSize("16")
NativeblocksButton(blockKey: "incrementBtn", label: buttonLabel)
.onClick {
script { getVariable, _, _ in
#"updateVariable("\#(count.key)", String(Number(\#(getVariable(count))) + 1));"#
}
}
}
.width("fill")
.alignmentHorizontal("center")
}
}
}
```
## How it works
1. Add the SDK to your Android or iOS project.
2. Annotate your components with `@NativeBlock` and `@NativeAction` and sync them with Nativeblocks servers.
3. Run `nativeblocks init` to authenticate and configure your project and generate the typed DSL.
4. Write screens using the DSL and deploy with `nativeblocks frame deploy`.
From your terminal to every user's phone. On every future deploy, users pick it up on their next app open.
---
### [Overview](https://nativeblocks.io/docs/sdk/overview/)
Nativeblocks is a Server-Driven UI (SDUI) platform for Android and iOS. Instead of hardcoding screens in your app, frames are defined server-side and rendered natively at runtime. Change what users see from the CLI. No rebuild, no release.
---
## What you can do
| Capability | Description |
|-----------|-------------|
| **Server-driven frames** | Define and update screens using the CLI. Ship UI changes instantly. |
| **Bring your own components** | Register any Compose or SwiftUI view as a block. Your design system, your components. |
| **A/B testing** | Deliver different frame variants based on user attributes (country, version, plan). |
| **Offline support** | Frames are cached locally. Users always see UI even without connectivity. |
| **Hot reload** | Connect your device to the CLI dev server and see changes live during development. No rebuilds. |
| **Event logging** | Hook into frame and block lifecycle events and pipe them to your analytics. |
| **Foundation blocks** | Optional prebuilt components (text, button, image, layout) for quick prototyping. |
---
## SDK packages
### Android
| Package | Purpose |
|---------|---------|
| `nativeblocks-android` | Core SDK: frame rendering, block/action registry, experiments |
| `nativeblocks-compiler-android` | KSP annotation processor: generates block/action schemas |
| `nativeblocks-wandkit-android` | DevKit for hot reload and log streaming |
| `nativeblocks-foundation-android` | Optional prebuilt Compose blocks |
### iOS
| Package | Purpose |
|---------|---------|
| `nativeblocks-ios-sdk` | Core SDK: frame rendering, block/action registry, experiments |
| `nativeblocks-compiler-ios` | Swift macro compiler: generates block/action schemas |
| `nativeblocks-wandkit-ios-sdk` | DevKit for hot reload and log streaming |
| `nativeblocks-foundation-ios` | Optional prebuilt SwiftUI blocks |
---
## How it works
1. **Annotate** your Compose/SwiftUI components with `@NativeBlock` and `@NativeAction`
2. **Build**: the compiler generates JSON schemas and registers your components
3. **Design** via the CLI: arrange blocks into frames, set properties
4. **Publish**: frames are delivered to your app at runtime
5. **Update anytime**: deploy a frame update via the CLI and users see it immediately
---
## Requirements
| | Android | iOS |
|-|---------|-----|
| Language | Kotlin 1.9.24+ | Swift 5.0+ |
| UI toolkit | Jetpack Compose | SwiftUI |
| Min OS | Android API 26 | iOS 15.0 |
---
### [Init SDK](https://nativeblocks.io/docs/sdk/init/)
## Add the Dependency
Root `settings.gradle`:
```groovy:settings.gradle showLineNumbers
repositories {
mavenCentral()
}
```
Module `build.gradle`:
```groovy:build.gradle showLineNumbers
dependencies {
implementation("io.nativeblocks:nativeblocks-android:1.8.1")
}
```
Add to your `Package.swift` or Xcode project:
```swift showLineNumbers
.package(url: "https://github.com/nativeblocks/nativeblocks-ios-sdk", from: "1.8.2")
```
---
## Initialize
Call `NativeblocksManager.initialize` in your `Application` class so it is ready before any Activity starts.
### Cloud Edition
```kotlin:App.kt showLineNumbers
class App : Application() {
override fun onCreate() {
super.onCreate()
NativeblocksManager.initialize(
applicationContext = this,
edition = NativeblocksEdition.Cloud(
endpoint = BuildConfig.NATIVEBLOCKS_API_URL,
apiKey = BuildConfig.NATIVEBLOCKS_API_KEY,
developmentMode = BuildConfig.DEBUG
)
)
}
}
```
### Community Edition
Load frames from your own server:
```kotlin:App.kt showLineNumbers
NativeblocksManager.initialize(
applicationContext = this,
edition = NativeblocksEdition.Community(
framesData = mapOf(
"/login" to "https://api.example.com/login.json",
"/profile" to "https://api.example.com/profile.json"
)
)
)
```
### Multi-Instance
```kotlin showLineNumbers
NativeblocksManager.initialize(
name = "main",
applicationContext = this,
edition = NativeblocksEdition.Cloud(...)
)
val manager = NativeblocksManager.getInstance("main")
```
Call `NativeblocksManager.initialize` in your `App` struct `init` so it runs before any view appears.
### Cloud Edition
```swift:MyApp.swift showLineNumbers
NativeblocksManager.initialize(
edition: .cloud(
endpoint: Bundle.main.infoDictionary?["NATIVEBLOCKS_API_URL"] as? String ?? "",
apiKey: Bundle.main.infoDictionary?["NATIVEBLOCKS_API_KEY"] as? String ?? "",
developmentMode: true
)
)
```
### Community Edition
Load frames from your own server:
```swift showLineNumbers
NativeblocksManager.initialize(
edition: .community(frameData: [
"/login": "https://api.example.com/login.json",
"/profile": "https://api.example.com/profile.json"
])
)
```
### Multi-Instance
```swift showLineNumbers
let mainManager = NativeblocksManager.initialize(
name: "main",
edition: .cloud(
endpoint: Bundle.main.infoDictionary?["NATIVEBLOCKS_API_URL"] as? String ?? "",
apiKey: Bundle.main.infoDictionary?["NATIVEBLOCKS_API_KEY"] as? String ?? "",
developmentMode: true
)
)
let manager = NativeblocksManager.getInstance(name: "main")
```
---
## Destroy
Clean up when your app terminates.
```kotlin:MainActivity.kt showLineNumbers
override fun onDestroy() {
super.onDestroy()
NativeblocksManager.getInstance().destroy()
}
```
```swift showLineNumbers
class AppDelegate: NSObject, UIApplicationDelegate {
func applicationWillTerminate(_ application: UIApplication) {
NativeblocksManager.getInstance().destroy()
}
}
```
---
## Global Parameters
Global parameters are sent with every frame request and used for A/B test targeting and conditional rendering:
```kotlin showLineNumbers
NativeblocksManager.getInstance().setGlobalParameters(
"language" to Locale.getDefault().language,
"country" to Locale.getDefault().country,
"appVersionCode" to BuildConfig.VERSION_CODE.toString(),
"plan" to "pro"
)
```
```swift showLineNumbers
NativeblocksManager.getInstance().setGlobalParameters([
"language": Locale.current.language.languageCode?.identifier ?? "en",
"country": Locale.current.region?.identifier ?? "US",
"appVersionCode": Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "0",
"plan": "pro"
])
```
---
## Full Example
A complete setup using an `Application` class and a single-Activity Compose app.
```kotlin:App.kt showLineNumbers
class App : Application() {
override fun onCreate() {
super.onCreate()
NativeblocksManager.initialize(
applicationContext = this,
edition = NativeblocksEdition.Cloud(
endpoint = BuildConfig.NATIVEBLOCKS_API_URL,
apiKey = BuildConfig.NATIVEBLOCKS_API_KEY,
developmentMode = BuildConfig.DEBUG
)
)
NativeblocksManager.getInstance().setGlobalParameters(
"language" to Locale.getDefault().language,
"country" to Locale.getDefault().country,
"appVersionCode" to BuildConfig.VERSION_CODE.toString()
)
}
}
```
```kotlin:MainActivity.kt showLineNumbers
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
NativeblocksFrame(
route = "/home",
routeArguments = hashMapOf(),
loading = { NativeblocksLoading() },
error = { message -> NativeblocksError(message = message) }
)
}
}
}
override fun onDestroy() {
super.onDestroy()
NativeblocksManager.getInstance().destroy()
}
}
```
A complete setup using a SwiftUI `App` entry point and an `AppDelegate` for teardown.
```swift:MyApp.swift showLineNumbers
import SwiftUI
import Nativeblocks
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
init() {
NativeblocksManager.initialize(
edition: .cloud(
endpoint: Bundle.main.infoDictionary?["NATIVEBLOCKS_API_URL"] as? String ?? "",
apiKey: Bundle.main.infoDictionary?["NATIVEBLOCKS_API_KEY"] as? String ?? "",
developmentMode: true
)
)
NativeblocksManager.getInstance().setGlobalParameters([
"language": Locale.current.language.languageCode?.identifier ?? "en",
"country": Locale.current.region?.identifier ?? "US",
"appVersionCode": Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "0"
])
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
```swift:AppDelegate.swift showLineNumbers
import UIKit
import Nativeblocks
class AppDelegate: NSObject, UIApplicationDelegate {
func applicationWillTerminate(_ application: UIApplication) {
NativeblocksManager.getInstance().destroy()
}
}
```
---
### [Show a Frame](https://nativeblocks.io/docs/sdk/show-frame/)
A **frame** is an embeddable, server-driven UI unit rendered natively at runtime. Frames are not limited to full screens. You can embed one anywhere in your existing app:
| Use case | Example |
|----------|---------|
| **Full screen** | Replace an entire Composable or SwiftUI view with a frame |
| **Card** | Drop a frame into a `Column` row or a SwiftUI `HSack` item |
| **Dialog / Alert** | Show a frame inside a `Dialog` |
| **Bottom sheet** | Embed a frame in a `bottomSheet` |
| **Section / Banner** | Render a frame as a partial section inside a larger screen |
`NativeblocksFrame` is just a Composable / SwiftUI view. Place it anywhere you would place any other UI component. The route tells it which frame definition to fetch and render.
---
## Scaffold
Before rendering any frame you can call `getScaffold` to fetch the scaffold model from the server. The scaffold returns registered frames. Use it to drive your initial navigation instead of hardcoding a route.
```kotlin:MainViewModel.kt showLineNumbers
class MainViewModel : ViewModel() {
fun loadScaffold() {
viewModelScope.launch {
NativeblocksManager.getInstance().getScaffold()
.onSuccess { scaffold ->
scaffold.frames
}
.onFailure { Log.e("Nativeblocks", "scaffold failed", it) }
}
}
}
```
```kotlin:MainActivity.kt showLineNumbers
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel: MainViewModel by viewModels()
setContent {
MyAppTheme {
LaunchedEffect(Unit) { viewModel.loadScaffold() }
NativeblocksFrame(
route = viewModel.startRoute,
routeArguments = hashMapOf(),
loading = { NativeblocksLoading() },
error = { msg -> NativeblocksError(message = msg) }
)
}
}
}
}
```
```swift:MainViewModel.swift showLineNumbers
import SwiftUI
import Nativeblocks
@MainActor
class MainViewModel: ObservableObject {
func loadScaffold() async {
let result = await NativeblocksManager.getInstance().getScaffold()
switch result {
case .success(let scaffold):
scaffold.frames
case .failure(let error):
print("[Nativeblocks] scaffold failed: \(error)")
}
}
}
```
```swift:ContentView.swift showLineNumbers
import SwiftUI
import Nativeblocks
struct ContentView: View {
@StateObject private var viewModel = MainViewModel()
var body: some View {
NativeblocksFrame(
route: viewModel.startRoute,
routeArguments: [:],
loading: { AnyView(NativeblocksLoading()) },
error: { message in AnyView(NativeblocksError(message: message)) }
)
.task { await viewModel.loadScaffold() }
}
}
```
```kotlin:MainActivity.kt showLineNumbers
setContent {
NativeblocksFrame(
route = "/",
routeArguments = hashMapOf(),
loading = { NativeblocksLoading() },
error = { message -> NativeblocksError(message = message) }
)
}
```
```swift:ContentView.swift showLineNumbers
struct ContentView: View {
var body: some View {
NativeblocksFrame(
route: "/",
routeArguments: [:],
loading: { AnyView(NativeblocksLoading()) },
error: { message in AnyView(NativeblocksError(message: message)) }
)
}
}
```
## Parameters
| Parameter | Description |
|-----------|-------------|
| `route` | The route key registered via the CLI (e.g. `/home`, `/profile`) |
| `routeArguments` | Runtime values injected into the frame (user ID, locale, flags) |
| `loading` | UI shown while the frame is fetching |
| `error` | UI shown if the frame fails to load |
---
## Passing Route Arguments
Route arguments let you inject dynamic values into a frame at render time. They can be read at runtime to conditionally show blocks or populate data:
```kotlin showLineNumbers
NativeblocksFrame(
route = "/profile",
routeArguments = hashMapOf(
"userId" to "abc123",
"tab" to "orders"
),
loading = { NativeblocksLoading() },
error = { message -> NativeblocksError(message = message) }
)
```
```swift showLineNumbers
NativeblocksFrame(
route: "/profile",
routeArguments: [
"userId": "abc123",
"tab": "orders"
],
loading: { AnyView(NativeblocksLoading()) },
error: { message in AnyView(NativeblocksError(message: message)) }
)
```
---
## Full Example
A single-Activity app using Jetpack Compose Navigation to render different frames per destination.
```kotlin:MainActivity.kt showLineNumbers
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Scaffold
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import io.nativeblocks.core.api.NativeblocksFrame
import io.nativeblocks.core.api.NativeblocksLoading
import io.nativeblocks.core.api.NativeblocksError
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
val navController = rememberNavController()
Scaffold { padding ->
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.padding(padding)
) {
composable("home") {
NativeblocksFrame(
route = "/home",
routeArguments = hashMapOf(),
loading = { NativeblocksLoading() },
error = { msg -> NativeblocksError(message = msg) }
)
}
composable("profile/{userId}") { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId") ?: ""
NativeblocksFrame(
route = "/profile",
routeArguments = hashMapOf("userId" to userId),
loading = { NativeblocksLoading() },
error = { msg -> NativeblocksError(message = msg) }
)
}
composable("settings") {
NativeblocksFrame(
route = "/settings",
routeArguments = hashMapOf(),
loading = { NativeblocksLoading() },
error = { msg -> NativeblocksError(message = msg) }
)
}
}
}
}
}
}
}
```
A SwiftUI app using `NavigationStack` to render different frames per screen.
```swift:MyApp.swift showLineNumbers
import SwiftUI
import Nativeblocks
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
```swift:ContentView.swift showLineNumbers
import SwiftUI
import Nativeblocks
struct ContentView: View {
var body: some View {
NavigationStack {
NativeblocksFrame(
route: "/home",
routeArguments: [:],
loading: { AnyView(NativeblocksLoading()) },
error: { message in AnyView(NativeblocksError(message: message)) }
)
.navigationDestination(for: String.self) { userId in
ProfileView(userId: userId)
}
}
}
}
```
```swift:ProfileView.swift showLineNumbers
import SwiftUI
import Nativeblocks
struct ProfileView: View {
let userId: String
var body: some View {
NativeblocksFrame(
route: "/profile",
routeArguments: ["userId": userId],
loading: { AnyView(NativeblocksLoading()) },
error: { message in AnyView(NativeblocksError(message: message)) }
)
}
}
```
```swift:SettingsView.swift showLineNumbers
import SwiftUI
import Nativeblocks
struct SettingsView: View {
var body: some View {
NativeblocksFrame(
route: "/settings",
routeArguments: [:],
loading: { AnyView(NativeblocksLoading()) },
error: { message in AnyView(NativeblocksError(message: message)) }
)
}
}
```
---
### [Block & Action](https://nativeblocks.io/docs/sdk/block-action/)
Blocks are UI components. Actions are business logic handlers. Both are annotated in your code, compiled into schemas, and registered with the SDK at runtime.
---
## Step 1: Add the Compiler
The compiler reads your annotations and generates JSON schemas that the CLI syncs to Nativeblocks.
Add to your module `build.gradle`:
```groovy:build.gradle showLineNumbers
dependencies {
implementation("io.nativeblocks:nativeblocks-android:1.8.1")
implementation("io.nativeblocks:nativeblocks-compiler-android:1.3.2")
ksp("io.nativeblocks:nativeblocks-compiler-android:1.3.2")
}
```
Also add the Gradle plugin to sync schemas via the CLI:
```groovy:build.gradle showLineNumbers
plugins {
id("io.nativeblocks.nativeblocks-gradle-plugin") version "1.2.1"
}
```
Add to your `Package.swift`:
```swift showLineNumbers
dependencies: [
.package(
url: "https://github.com/nativeblocks/nativeblocks-compiler-ios",
.upToNextMajor(from: "1.3.2")
),
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "NativeblocksCompiler", package: "nativeblocks-compiler-ios")
]
)
]
```
---
## Step 2: Annotate a Block
```kotlin showLineNumbers
@NativeBlock(
name = "Custom Button",
keyType = "myOrg/CUSTOM_BUTTON",
description = "A configurable button block",
version = 1,
versionName = "1.0.0"
)
@Composable
fun CustomButton(
@NativeBlockData(description = "Button label")
text: String,
@NativeBlockProp(
description = "Size variant",
valuePicker = NativeBlockValuePicker.DROPDOWN,
valuePickerOptions = [
NativeBlockValuePickerOption("S", "Small"),
NativeBlockValuePickerOption("M", "Medium"),
NativeBlockValuePickerOption("L", "Large"),
]
)
size: String = "M",
@NativeBlockSlot(description = "Leading icon")
onLeadingIcon: @Composable (index: BlockIndex) -> Unit,
@NativeBlockEvent(description = "On click")
onClick: () -> Unit,
) {
// your Composable implementation
}
```
After building, the compiler generates a provider. Register it after SDK init:
```kotlin showLineNumbers
DemoBlockProvider.provideBlocks()
```
```swift showLineNumbers
@NativeBlock(
name: "Custom Button",
keyType: "myOrg/CUSTOM_BUTTON",
description: "A configurable button block",
version: 1,
versionName: "1.0.0"
)
struct CustomButton: View {
@NativeBlockData(description: "Button label")
var text: String
@NativeBlockProp(
description: "Size variant",
valuePicker: NativeBlockValuePicker.DROPDOWN,
valuePickerOptions: [
NativeBlockValuePickerOption("S", "Small"),
NativeBlockValuePickerOption("M", "Medium"),
NativeBlockValuePickerOption("L", "Large"),
]
)
var size: String = "M"
@NativeBlockSlot(description: "Leading icon")
var onLeadingIcon: () -> AnyView
@NativeBlockEvent(description: "On click")
var onClick: (() -> Void)?
var body: some View { /* your SwiftUI view */ }
}
```
After building, register the generated provider:
```swift showLineNumbers
MyAppBlockProvider.provideBlocks()
```
### Annotation reference
| Annotation | Target | Purpose |
|-----------|--------|---------|
| `@NativeBlock` | Function / Struct | Marks the component as a block with a unique `keyType` |
| `@NativeBlockData` | Parameter | Runtime variable bound to frame state |
| `@NativeBlockProp` | Parameter | Static property configured via the CLI; supports `valuePicker`, `valuePickerGroup`, `valuePickerOptions`, and `defaultValue` |
| `@NativeBlockSlot` | Parameter | Slot where child blocks can be nested |
| `@NativeBlockEvent` | Parameter | Event the block can fire (tap, change, etc.); supports `dataBinding` |
| `@NativeBlockValuePickerOption` | — | Defines a selectable option (`id`, `text`) for `DROPDOWN` or `COMBOBOX_INPUT` pickers |
| `@NativeBlockValuePickerPosition` | — | Groups related properties under a named section in the CLI UI |
---
## Step 3: Annotate an Action
```kotlin showLineNumbers
@NativeAction(
keyType = "myOrg/NAVIGATE_ACTION",
name = "Navigate",
description = "Navigates to a new screen",
version = 1,
versionName = "1.0.0"
)
class NavigateAction {
@NativeActionParameter
data class Parameter(
@NativeActionProp(description = "Destination route")
val route: String = "",
@NativeActionData(description = "Error message stored in a variable")
val error: String = "",
@NativeActionEvent(then = Then.SUCCESS)
val onSuccess: () -> Unit,
@NativeActionEvent(then = Then.FAILURE, dataBinding = ["error"])
val onFailure: (String) -> Unit,
)
@NativeActionFunction
suspend fun navigate(parameter: Parameter) {
try {
// perform navigation
parameter.onSuccess()
} catch (e: Exception) {
parameter.onFailure(e.message ?: "Unknown error")
}
}
}
```
Register after SDK init:
```kotlin showLineNumbers
DemoActionProvider.provideActions()
```
```swift showLineNumbers
@NativeAction(
keyType: "myOrg/NAVIGATE_ACTION",
name: "Navigate",
description: "Navigates to a new screen",
version: 1,
versionName: "1.0.0"
)
class NavigateAction {
@NativeActionParameter
struct Parameter {
@NativeActionProp(description: "Destination route")
var route: String = ""
@NativeActionData(description: "Error message stored in a variable")
var error: String = ""
@NativeActionEvent(then: .success)
var onSuccess: () -> Void
@NativeActionEvent(then: .failure, dataBinding: ["error"])
var onFailure: (String) -> Void
}
@NativeActionFunction
func navigate(parameter: Parameter) {
do {
// perform navigation
parameter.onSuccess()
} catch {
parameter.onFailure(error.localizedDescription)
}
}
}
```
Register after SDK init:
```swift showLineNumbers
MyAppActionProvider.provideActions()
```
### Annotation reference
| Annotation | Target | Purpose |
|-----------|--------|---------|
| `@NativeAction` | Class | Marks the class as an action with a unique `keyType` |
| `@NativeActionParameter` | Class | Marks a class as a parameter holder passed to the action at runtime |
| `@NativeActionFunction` | Function | Marks the function that executes the action logic |
| `@NativeActionProp` | Parameter | Static property configured via the CLI; supports `valuePicker`, `valuePickerGroup`, `valuePickerOptions`, and `defaultValue` |
| `@NativeActionData` | Parameter | Runtime variable bound to frame state |
| `@NativeActionEvent` | Parameter | Outcome event the action can emit; `then` controls flow (`SUCCESS`, `FAILURE`, `NEXT`, `END`) |
| `@NativeActionValuePickerOption` | — | Defines a selectable option (`id`, `text`) for `DROPDOWN` pickers |
| `@NativeActionValuePickerPosition` | — | Groups related properties under a named section in the CLI UI |
---
## Step 4: Register at Runtime
You can also register blocks and actions manually:
```kotlin showLineNumbers
// Block
NativeblocksManager.getInstance().provideBlock(
blockType = "myOrg/CUSTOM_BUTTON",
block = { props -> CustomButton(props) }
)
// Action
NativeblocksManager.getInstance().provideAction(
actionType = "myOrg/NAVIGATE_ACTION",
action = NavigateAction()
)
```
```swift showLineNumbers
// Block
NativeblocksManager.getInstance().provideBlock(
blockKeyType: "myOrg/CUSTOM_BUTTON",
block: { props in CustomButton(props: props) }
)
// Action
NativeblocksManager.getInstance().provideAction(
actionKeyType: "myOrg/NAVIGATE_ACTION",
action: NavigateAction()
)
```
---
## Type Converters
Frame properties are stored as JSON strings in the server. A type converter bridges between that raw string and a typed Kotlin or Swift value so you can use rich types directly in your block properties.
**Important:** Type converters only apply to `@NativeBlockProp` (static properties configured via the CLI). They do not apply to `@NativeBlockData` (runtime data variables bound to frame state).
### Implement a converter
Implement `INativeType` with two methods: `toString` to serialise your type to a string, and `fromString` to deserialise it back. A common property use case is a `Color` stored as a hex string in the frame definition.
```kotlin showLineNumbers
class ColorNativeType : INativeType {
override fun toString(value: Color): String {
val argb = value.toArgb()
return String.format("#%08X", argb)
}
override fun fromString(value: String): Color {
return try {
Color(android.graphics.Color.parseColor(value))
} catch (e: Exception) {
Color.Unspecified
}
}
}
```
Implement `INativeType` with two methods: `toString` to serialise your type to a string, and `fromString` to deserialise it back. A common property use case is a `Color` stored as a hex string in the frame definition.
```swift showLineNumbers
class ColorNativeType: INativeType {
func toString(value: Color) -> String {
guard let components = UIColor(value).cgColor.components, components.count >= 3 else {
return "#000000"
}
let r = Int(components[0] * 255)
let g = Int(components[1] * 255)
let b = Int(components[2] * 255)
return String(format: "#%02X%02X%02X", r, g, b)
}
func fromString(value: String) -> Color {
var hex = value.trimmingCharacters(in: .init(charactersIn: "#"))
guard hex.count == 6, let rgb = UInt64(hex, radix: 16) else { return .clear }
return Color(
red: Double((rgb >> 16) & 0xFF) / 255,
green: Double((rgb >> 8) & 0xFF) / 255,
blue: Double(rgb & 0xFF) / 255
)
}
}
```
### Register a converter
Call `provideTypeConverter` after SDK initialisation, before any blocks are rendered.
```kotlin showLineNumbers
NativeblocksManager.getInstance().provideTypeConverter(
type = Color::class,
converter = ColorNativeType()
)
```
```swift showLineNumbers
NativeblocksManager.getInstance().provideTypeConverter(
Color.self,
converter: ColorNativeType()
)
```
### Use it in a block property
Once registered, annotate a block property with your custom type. The SDK calls the converter automatically when reading from frame JSON.
```kotlin showLineNumbers
@NativeBlock(name = "Custom Button", keyType = "myOrg/CUSTOM_BUTTON", version = 1, versionName = "1.0.0")
@Composable
fun CustomButton(
@NativeBlockData(description = "Button label")
text: String,
@NativeBlockProp(description = "Background color as hex (e.g. #FF6200EE)")
backgroundColor: Color = Color.Blue,
) {
Box(modifier = Modifier.background(backgroundColor)) {
Text(text = text)
}
}
```
```swift showLineNumbers
@NativeBlock(name: "Custom Button", keyType: "myOrg/CUSTOM_BUTTON", version: 1, versionName: "1.0.0")
struct CustomButton: View {
@NativeBlockData(description: "Button label")
var text: String
@NativeBlockProp(description: "Background color as hex (e.g. #FF6200EE)")
var backgroundColor: Color = .blue
var body: some View {
Text(text)
.padding()
.background(backgroundColor)
}
}
```
---
### [Fallback Handling](https://nativeblocks.io/docs/sdk/fallback/)
When a frame is fetched from the server, it may reference a block type or action type that is not registered in the current version of your app. This can happen when:
- A new block or action was added via the CLI but the user has not updated the app yet
- A block or action was removed from the binary but old frame definitions still reference it
- A frame is deployed to a version of the app that does not have the matching registration
Without a fallback, unrecognised types fail silently or cause a blank space in the UI. Registering fallback handlers lets you control exactly what happens in those cases.
---
## Fallback Block
A fallback block is rendered in place of any block whose `keyType` is not found in the registry.
```kotlin showLineNumbers
NativeblocksManager.getInstance().provideFallbackBlock { keyType, name ->
Box(Modifier.padding(8.dp)) {
Text(
text = "Missing block: $keyType ($name)",
color = Color.Red
)
}
}
```
```swift showLineNumbers
NativeblocksManager.getInstance().provideFallbackBlock { keyType, name in
AnyView(
Text("Missing block: \(keyType) (\(name))")
.foregroundColor(.red)
.padding(8)
)
}
```
---
## Fallback Action
A fallback action is invoked whenever an action type is triggered that has no registered handler.
```kotlin showLineNumbers
NativeblocksManager.getInstance().provideFallbackAction { keyType, name ->
Log.w("Nativeblocks", "Unhandled action: $keyType ($name)")
}
```
```swift showLineNumbers
NativeblocksManager.getInstance().provideFallbackAction { keyType, name in
print("Unhandled action: \(keyType) (\(name))")
}
```
---
## Production pattern: prompt users to update
Show users they need to update rather than hiding content silently. For missing blocks, show an inline prompt. For missing actions, present an alert or bottom sheet.
### Fallback block
```kotlin:App.kt showLineNumbers
NativeblocksManager.getInstance().provideFallbackBlock { _, _ ->
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Update required",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = "This content is not supported in your current app version. Update to see it.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
TextButton(onClick = {
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=${context.packageName}")
)
context.startActivity(intent)
}) {
Text("Update now")
}
}
}
```
```swift:MyApp.swift showLineNumbers
NativeblocksManager.getInstance().provideFallbackBlock { _, _ in
AnyView(UpdatePromptView())
}
```
```swift:UpdatePromptView.swift showLineNumbers
import SwiftUI
struct UpdatePromptView: View {
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Update required")
.font(.subheadline).bold()
Text("This content is not supported in your current app version. Update to see it.")
.font(.caption)
.foregroundColor(.secondary)
Link("Update now", destination: URL(string: "https://apps.apple.com/app/idYOUR_APP_ID")!)
.font(.caption).bold()
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.horizontal, 16)
}
}
```
### Fallback action
```kotlin:App.kt showLineNumbers
NativeblocksManager.getInstance().provideFallbackAction { _, _ ->
val context = LocalContext.current
AlertDialog.Builder(context)
.setTitle("Update required")
.setMessage("To use this feature your app needs to be updated to the latest version.")
.setPositiveButton("Update") {
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=${context.packageName}")
)
context.startActivity(intent)
}
.setNegativeButton("Cancel", null)
.show()
}
```
```swift:MyApp.swift showLineNumbers
NativeblocksManager.getInstance().provideFallbackAction { _, _ in
DispatchQueue.main.async {
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let root = scene.windows.first?.rootViewController else { return }
let alert = UIAlertController(
title: "Update required",
message: "To use this feature your app needs to be updated to the latest version.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Update", style: .default) { _ in
if let url = URL(string: "https://apps.apple.com/app/idYOUR_APP_ID") {
UIApplication.shared.open(url)
}
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
root.present(alert, animated: true)
}
}
```
---
## Debug vs. production
Use `BuildConfig.DEBUG` / `#if DEBUG` to switch between a developer-visible error and the user-facing update prompt.
```kotlin:App.kt showLineNumbers
if (BuildConfig.DEBUG) {
NativeblocksManager.getInstance().provideFallbackBlock { keyType, name ->
Box(
Modifier
.fillMaxWidth()
.padding(8.dp)
.border(1.dp, Color.Red, RoundedCornerShape(4.dp))
.padding(8.dp)
) {
Text("Missing block: $keyType ($name)", color = Color.Red, fontSize = 12.sp)
}
}
NativeblocksManager.getInstance().provideFallbackAction { keyType, name ->
Log.w("Nativeblocks", "Unhandled action: $keyType ($name)")
}
} else {
NativeblocksManager.getInstance().provideFallbackBlock { _, _ ->
// inline update prompt (see above)
}
NativeblocksManager.getInstance().provideFallbackAction { _, _ ->
// update alert (see above)
}
}
```
```swift:MyApp.swift showLineNumbers
#if DEBUG
NativeblocksManager.getInstance().provideFallbackBlock { keyType, name in
AnyView(
Text("Missing block: \(keyType) (\(name))")
.font(.caption).foregroundColor(.red).padding(8)
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.red, lineWidth: 1))
)
}
NativeblocksManager.getInstance().provideFallbackAction { keyType, name in
print("[Nativeblocks] Unhandled action: \(keyType) (\(name))")
}
#else
NativeblocksManager.getInstance().provideFallbackBlock { _, _ in
AnyView(UpdatePromptView()) // inline update prompt (see above)
}
NativeblocksManager.getInstance().provideFallbackAction { _, _ in
// update alert (see above)
}
#endif
```
---
## Parameters
| Parameter | Description |
|-----------|-------------|
| `keyType` | The unique type identifier of the missing block or action |
| `name` | The specific name/key assigned to block/action in the frame definition |
---
### [Cache & Sync](https://nativeblocks.io/docs/sdk/cache/)
Nativeblocks caches frames locally after the first fetch. On subsequent loads the cached version is shown immediately while the SDK silently checks for updates in the background.
---
## API
```kotlin showLineNumbers
// Fetch the latest frame from the server and update local cache
NativeblocksManager.getInstance().syncFrame("/home")
// Remove a specific frame from cache
NativeblocksManager.getInstance().clearFrame("/home")
// Remove all cached frames
NativeblocksManager.getInstance().clearAllFrames()
```
```swift showLineNumbers
// Fetch the latest frame from the server and update local cache
await NativeblocksManager.getInstance().syncFrame(route: "/home")
// Remove a specific frame from cache
await NativeblocksManager.getInstance().clearFrame(route: "/home")
// Remove all cached frames
await NativeblocksManager.getInstance().clearAllFrames()
```
---
## Scenarios
### App Launch Prefetch
Fetch critical frames during startup so users see fresh content on first navigation with no loading delay.
Use `getScaffold` to retrieve all registered frame routes from the server, then sync each one. This way your prefetch list stays in sync with whatever frames are deployed without hardcoding routes in the app.
```kotlin:HomeViewModel.kt showLineNumbers
class HomeViewModel : ViewModel() {
fun prefetch() {
viewModelScope.launch {
// Fetch all registered routes from the server and sync each one
NativeblocksManager.getInstance().getScaffold()
.onSuccess { scaffold ->
scaffold.frames.forEach { frame ->
NativeblocksManager.getInstance().syncFrame(frame.route)
}
}
.onFailure { Log.e("Nativeblocks", "prefetch failed", it) }
}
}
}
```
```kotlin:MainActivity.kt showLineNumbers
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel: HomeViewModel by viewModels()
setContent {
MyAppTheme {
val navController = rememberNavController()
LaunchedEffect(Unit) {
viewModel.prefetch()
}
Scaffold { padding ->
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.padding(padding)
) {
composable("home") {
NativeblocksFrame(
route = "/home",
routeArguments = hashMapOf(),
loading = { NativeblocksLoading() },
error = { msg -> NativeblocksError(message = msg) }
)
}
}
}
}
}
}
}
```
```swift:ContentView.swift showLineNumbers
import SwiftUI
import Nativeblocks
struct ContentView: View {
var body: some View {
NativeblocksFrame(
route: "/home",
routeArguments: [:],
loading: { AnyView(NativeblocksLoading()) },
error: { message in AnyView(NativeblocksError(message: message)) }
)
.task {
// Fetch all registered routes from the server and sync each one
let result = await NativeblocksManager.getInstance().getScaffold()
if case .success(let scaffold) = result {
await withTaskGroup(of: Void.self) { group in
for frame in scaffold.frames {
group.addTask {
await NativeblocksManager.getInstance().syncFrame(route: frame.route)
}
}
}
}
}
}
}
```
---
### Pull-to-Refresh
Let users manually refresh a frame by pulling down on the screen.
```kotlin:HomeScreen.kt showLineNumbers
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
@Composable
fun HomeScreen() {
var isRefreshing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = {
scope.launch {
isRefreshing = true
NativeblocksManager.getInstance().syncFrame("/home")
isRefreshing = false
}
}
) {
NativeblocksFrame(
route = "/home",
routeArguments = hashMapOf(),
loading = { NativeblocksLoading() },
error = { msg -> NativeblocksError(message = msg) }
)
}
}
```
```swift:HomeView.swift showLineNumbers
import SwiftUI
import Nativeblocks
struct HomeView: View {
var body: some View {
ScrollView {
NativeblocksFrame(
route: "/home",
routeArguments: [:],
loading: { AnyView(NativeblocksLoading()) },
error: { message in AnyView(NativeblocksError(message: message)) }
)
}
.refreshable {
await NativeblocksManager.getInstance().syncFrame(route: "/home")
}
}
}
```
---
### After a Server Deploy
When you push a new frame version, use a push notification to trigger an immediate sync on all active clients.
```kotlin:NativeblocksMessagingService.kt showLineNumbers
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class NativeblocksMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
val route = message.data["route"] ?: return
GlobalScope.launch {
NativeblocksManager.getInstance().syncFrame(route)
}
}
}
```
```swift:AppDelegate.swift showLineNumbers
import UIKit
import Nativeblocks
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]
) async -> UIBackgroundFetchResult {
guard let route = userInfo["route"] as? String else {
return .noData
}
await NativeblocksManager.getInstance().syncFrame(route: route)
return .newData
}
}
```
---
### User Logout
Clear all cached frames on logout to remove any user-scoped or personalised content.
```kotlin:AuthViewModel.kt showLineNumbers
fun logout() {
viewModelScope.launch {
authRepository.logout()
NativeblocksManager.getInstance().clearAllFrames()
navController.navigate("login") {
popUpTo(0) { inclusive = true }
}
}
}
```
```swift:AuthViewModel.swift showLineNumbers
func logout() async {
await authRepository.logout()
await NativeblocksManager.getInstance().clearAllFrames()
router.navigate(to: .login)
}
```
---
### Stale or Broken Frame
If a frame fails to render, clear its cache from the error callback and let it re-fetch on the next visit.
```kotlin:CheckoutScreen.kt showLineNumbers
@Composable
fun CheckoutScreen() {
val scope = rememberCoroutineScope()
NativeblocksFrame(
route = "/checkout",
routeArguments = hashMapOf(),
loading = { NativeblocksLoading() },
error = { msg ->
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = msg)
Spacer(modifier = Modifier.height(12.dp))
Button(onClick = {
scope.launch {
NativeblocksManager.getInstance().clearFrame("/checkout")
}
}) {
Text("Retry")
}
}
}
)
}
```
```swift:CheckoutView.swift showLineNumbers
import SwiftUI
import Nativeblocks
struct CheckoutView: View {
var body: some View {
NativeblocksFrame(
route: "/checkout",
routeArguments: [:],
loading: { AnyView(NativeblocksLoading()) },
error: { message in
AnyView(
VStack(spacing: 12) {
Text(message)
Button("Retry") {
Task {
await NativeblocksManager.getInstance()
.clearFrame(route: "/checkout")
}
}
}
)
}
)
}
}
```
---
### A/B Test Reset
When user attributes change (plan upgrade, login), update global parameters and sync to receive the correct variant immediately.
```kotlin:ProfileViewModel.kt showLineNumbers
fun onPlanUpgraded(newPlan: String) {
viewModelScope.launch {
NativeblocksManager.getInstance().setGlobalParameters(
"plan" to newPlan
)
NativeblocksManager.getInstance().syncFrame("/home")
}
}
```
```swift:ProfileViewModel.swift showLineNumbers
func onPlanUpgraded(newPlan: String) async {
NativeblocksManager.getInstance().setGlobalParameters(["plan": newPlan])
await NativeblocksManager.getInstance().syncFrame(route: "/home")
}
```
---
## How offline works
When a device has no connectivity, `NativeblocksFrame` renders the last cached version automatically. No code changes needed. The frame stays cached until explicitly cleared or until it expires based on your release TTL settings.
If no cached version exists and the device is offline, the `error` callback is triggered.
---
### [A/B Testing](https://nativeblocks.io/docs/sdk/experiments/)
A/B testing in Nativeblocks works at the **frame level**. Via the CLI, you define multiple frame variants and set targeting conditions. The SDK sends **global parameters** with every frame request, and the server picks the right variant based on your rules. No code changes needed per test.
---
## How it works
1. Via the CLI, create two or more variants of a frame (e.g. `/home-v1`, `/home-v2`, or a single frame with conditional blocks)
2. Set targeting conditions via the Studio (percentage split, user attributes, etc.)
3. In your app, set global parameters that represent the current user's context
4. The SDK sends those parameters on every frame request: the server picks the right variant
---
## Set Global Parameters
Global parameters are key-value pairs attached to every frame request. Set them once after init and update them when the user's context changes.
```kotlin showLineNumbers
NativeblocksManager.getInstance().setGlobalParameters(
"country" to "US",
"language" to "EN",
"appVersionCode" to "45",
"plan" to "pro",
"isLoggedIn" to "true"
)
```
```swift showLineNumbers
NativeblocksManager.getInstance().setGlobalParameters([
"country": "US",
"language": "EN",
"appVersionCode": "45",
"plan": "pro",
"isLoggedIn": "true"
])
```
---
## Targeting examples
| Goal | Global parameter to set | Server condition |
|------|------------------------|-----------------|
| Show a promo banner only to US users | `"country": "US"` | `country == "US"` |
| Roll out a new checkout flow to 20% | (none) | Percentage split 20/80 |
| Beta feature for paid users | `"plan": "pro"` | `plan == "pro"` |
| Version-gate a redesign | `"appVersionCode": "50"` | `appVersionCode >= "50"` |
| Language-specific onboarding | `"language": "DE"` | `language == "DE"` |
---
## Scenario examples
### Country-based promo banner
Show a promotional home frame only to users in a specific country. All other users see the default frame. The targeting rule is configured entirely via the Studio.
```kotlin:App.kt showLineNumbers
class App : Application() {
override fun onCreate() {
super.onCreate()
NativeblocksManager.initialize(
applicationContext = this,
edition = NativeblocksEdition.Cloud(
endpoint = BuildConfig.NATIVEBLOCKS_API_URL,
apiKey = BuildConfig.NATIVEBLOCKS_API_KEY,
developmentMode = BuildConfig.DEBUG
)
)
NativeblocksManager.getInstance().setGlobalParameters(
"country" to Locale.getDefault().country
)
}
}
```
```kotlin:HomeScreen.kt showLineNumbers
@Composable
fun HomeScreen() {
NativeblocksFrame(
route = "/home",
routeArguments = hashMapOf(),
loading = { NativeblocksLoading() },
error = { msg -> NativeblocksError(message = msg) }
)
}
```
In the Studio, configure the condition `country == "US"` on the promo variant of `/home`. Users in the US receive the promo frame, everyone else receives the default.
```swift:MyApp.swift showLineNumbers
@main
struct MyApp: App {
init() {
NativeblocksManager.initialize(
edition: .cloud(
endpoint: Bundle.main.infoDictionary?["NATIVEBLOCKS_API_URL"] as? String ?? "",
apiKey: Bundle.main.infoDictionary?["NATIVEBLOCKS_API_KEY"] as? String ?? "",
developmentMode: true
)
)
NativeblocksManager.getInstance().setGlobalParameters([
"country": Locale.current.region?.identifier ?? "US"
])
}
var body: some Scene {
WindowGroup {
HomeView()
}
}
}
```
```swift:HomeView.swift showLineNumbers
import SwiftUI
import Nativeblocks
struct HomeView: View {
var body: some View {
NativeblocksFrame(
route: "/home",
routeArguments: [:],
loading: { AnyView(NativeblocksLoading()) },
error: { message in AnyView(NativeblocksError(message: message)) }
)
}
}
```
In the Studio, configure the condition `country == "US"` on the promo variant of `/home`.
---
### Plan-based feature gate
Show a premium feature frame only to users on the `pro` plan. After a user upgrades, update the parameter and sync so they see the new content immediately.
```kotlin:AuthViewModel.kt showLineNumbers
fun onLoginSuccess(user: User) {
NativeblocksManager.getInstance().setGlobalParameters(
"userId" to user.id,
"plan" to user.plan,
"isLoggedIn" to "true"
)
navController.navigate("home")
}
fun onPlanUpgraded(newPlan: String) {
viewModelScope.launch {
NativeblocksManager.getInstance().setGlobalParameters(
"plan" to newPlan
)
NativeblocksManager.getInstance().syncFrame("/home")
NativeblocksManager.getInstance().syncFrame("/dashboard")
}
}
```
```kotlin:DashboardScreen.kt showLineNumbers
@Composable
fun DashboardScreen() {
NativeblocksFrame(
route = "/dashboard",
routeArguments = hashMapOf(),
loading = { NativeblocksLoading() },
error = { msg -> NativeblocksError(message = msg) }
)
}
```
In the Studio, configure `plan == "pro"` on the premium dashboard variant. Free users see the standard variant automatically.
```swift:AuthViewModel.swift showLineNumbers
func onLoginSuccess(user: User) {
NativeblocksManager.getInstance().setGlobalParameters([
"userId": user.id,
"plan": user.plan,
"isLoggedIn": "true"
])
router.navigate(to: .home)
}
func onPlanUpgraded(newPlan: String) async {
NativeblocksManager.getInstance().setGlobalParameters(["plan": newPlan])
await NativeblocksManager.getInstance().syncFrame(route: "/home")
await NativeblocksManager.getInstance().syncFrame(route: "/dashboard")
}
```
```swift:DashboardView.swift showLineNumbers
import SwiftUI
import Nativeblocks
struct DashboardView: View {
var body: some View {
NativeblocksFrame(
route: "/dashboard",
routeArguments: [:],
loading: { AnyView(NativeblocksLoading()) },
error: { message in AnyView(NativeblocksError(message: message)) }
)
}
}
```
In the Studio, configure `plan == "pro"` on the premium dashboard variant.
---
### Version-gate a redesign
Roll out a new UI only to users on a minimum app version. Older versions keep the old design automatically.
```kotlin:App.kt showLineNumbers
NativeblocksManager.getInstance().setGlobalParameters(
"appVersionCode" to BuildConfig.VERSION_CODE.toString()
)
```
```kotlin:HomeScreen.kt showLineNumbers
@Composable
fun HomeScreen() {
NativeblocksFrame(
route = "/home",
routeArguments = hashMapOf(),
loading = { NativeblocksLoading() },
error = { msg -> NativeblocksError(message = msg) }
)
}
```
In the Studio, configure `appVersionCode >= "50"` on the redesigned frame variant. Users on older builds see the previous design with no app update required on your side.
```swift:MyApp.swift showLineNumbers
NativeblocksManager.getInstance().setGlobalParameters([
"appVersionCode": Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "0"
])
```
```swift:HomeView.swift showLineNumbers
import SwiftUI
import Nativeblocks
struct HomeView: View {
var body: some View {
NativeblocksFrame(
route: "/home",
routeArguments: [:],
loading: { AnyView(NativeblocksLoading()) },
error: { message in AnyView(NativeblocksError(message: message)) }
)
}
}
```
In the Studio, configure `appVersionCode >= "50"` on the redesigned variant.
---
## Update parameters at runtime
Parameters can be updated any time: after login, on settings change, etc. The next frame fetch uses the updated values.
```kotlin showLineNumbers
// After user logs in
NativeblocksManager.getInstance().setGlobalParameters(
"userId" to user.id,
"plan" to user.plan,
"isLoggedIn" to "true"
)
// Sync the home frame to pick up the new variant immediately
NativeblocksManager.getInstance().syncFrame("/home")
```
```swift showLineNumbers
// After user logs in
NativeblocksManager.getInstance().setGlobalParameters([
"userId": user.id,
"plan": user.plan,
"isLoggedIn": "true"
])
// Sync the home frame to pick up the new variant immediately
await NativeblocksManager.getInstance().syncFrame(route: "/home")
```
---
## Preview Kit
PreviewKit is a QA and stakeholder tool that lets you override global parameters at runtime without touching code or CLI targeting rules. Shake the device to open a parameter form, adjust any value, and the frame re-fetches immediately to deliver the matching variant.
**Important:** PreviewKit only works with `developmentMode = false` (production mode) and the Cloud edition only.
### Add the dependency
```groovy:build.gradle showLineNumbers
dependencies {
implementation("io.nativeblocks:nativeblocks-wandkit-android:1.2.0")
}
```
```swift showLineNumbers
.package(url: "https://github.com/nativeblocks/nativeblocks-wandkit-ios-sdk", from: "1.2.0")
```
### Initialize
Register PreviewKit after SDK init instead of DevKit. Use it only in production or internal QA builds.
```kotlin:MainActivity.kt showLineNumbers
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NativeblocksManager.initialize(
applicationContext = this,
edition = NativeblocksEdition.Cloud(
endpoint = BuildConfig.NATIVEBLOCKS_API_URL,
apiKey = BuildConfig.NATIVEBLOCKS_API_KEY,
developmentMode = false
)
)
val previewKit = PreviewKit.Builder()
.launchOnShake()
.build()
NativeblocksManager.getInstance().wandKit(previewKit)
}
```
```swift:MyApp.swift showLineNumbers
@main
struct MyApp: App {
let previewKit: PreviewKit
init() {
NativeblocksManager.initialize(
edition: .cloud(
endpoint: Bundle.main.infoDictionary?["NATIVEBLOCKS_API_URL"] as? String ?? "",
apiKey: Bundle.main.infoDictionary?["NATIVEBLOCKS_API_KEY"] as? String ?? "",
developmentMode: false
)
)
previewKit = PreviewKit.Builder()
.launchOnShake()
.build()
NativeblocksManager.getInstance().wandKit(previewKit)
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
```
### How to use
1. Install a production build on a real device
2. Shake the device. The parameter form opens.
3. Change any global parameter (e.g. set `plan` to `pro`)
4. Confirm. The frame re-fetches using the overridden values and delivers the matching variant.
You can also launch the form manually from a hidden settings button or debug menu:
```kotlin showLineNumbers
previewKit.launch()
```
```swift showLineNumbers
previewKit.launch()
```
| Builder option | What it does |
|----------------|-------------|
| `launchOnShake()` | Opens the parameter form when the device is shaken |
---
### [Logging](https://nativeblocks.io/docs/sdk/debugging/)
Register a logger to capture SDK events and forward them to Firebase, Amplitude, PostHog, or any other service.
---
## Implement a Logger
```kotlin showLineNumbers
class AnalyticsLogger : INativeLogger {
override fun log(
level: LoggerEventLevel,
event: String,
message: String,
parameters: Map
) {
when (level) {
LoggerEventLevel.INFO -> {
// Firebase Analytics
Firebase.analytics.logEvent(event) {
parameters.forEach { (key, value) -> param(key, value) }
}
// Amplitude
Amplitude.getInstance().logEvent(event, JSONObject(parameters))
// PostHog
PostHog.capture(event, properties = parameters)
}
LoggerEventLevel.WARNING -> {
MyMonitoring.warn(tag = event, message = message, extra = parameters)
}
LoggerEventLevel.ERROR -> {
// Crashlytics
Crashlytics.log("[$event] $message")
Crashlytics.setCustomKeys { parameters.forEach { (k, v) -> key(k, v) } }
}
}
}
}
```
```swift showLineNumbers
class AnalyticsLogger: INativeLogger {
func log(
level: LoggerEventLevel,
event: String,
message: String,
parameters: [String: String]
) {
switch level {
case .info:
// Firebase Analytics
Analytics.logEvent(event, parameters: parameters)
// Amplitude
Amplitude.instance().logEvent(event, withEventProperties: parameters)
// PostHog
PostHogSDK.shared.capture(event, properties: parameters)
case .warning:
MyMonitoring.warn(tag: event, message: message, extra: parameters)
case .error:
// Crashlytics
Crashlytics.crashlytics().log("[\(event)] \(message)")
parameters.forEach { Crashlytics.crashlytics().setCustomValue($1, forKey: $0) }
}
}
}
```
---
## Register the Logger
```kotlin showLineNumbers
NativeblocksManager.getInstance().provideEventLogger(
loggerType = "ANALYTICS_LOGGER",
logger = AnalyticsLogger()
)
```
```swift showLineNumbers
NativeblocksManager.getInstance().provideEventLogger(
loggerType: "ANALYTICS_LOGGER",
logger: AnalyticsLogger()
)
```
---
## Log levels
| Level | When it fires |
|-------|--------------|
| `INFO` | Normal lifecycle: frame loaded, block rendered, action triggered |
| `WARNING` | Non-critical issues: missing optional block, slow network response |
| `ERROR` | Failures: frame not found, action crashed, network error |
---
### [Hot Reload](https://nativeblocks.io/docs/sdk/hot-reload/)
DevKit connects your running app to the Nativeblocks CLI. When you deploy a change via the CLI, the frame updates on your device instantly. No rebuild required. It also streams all SDK events to the CLI console for real-time debugging.
---
## Add the Dependency
```groovy:build.gradle showLineNumbers
dependencies {
debugImplementation("io.nativeblocks:nativeblocks-wandkit-android:1.2.0")
}
```
```swift showLineNumbers
.package(url: "https://github.com/nativeblocks/nativeblocks-wandkit-ios-sdk", from: "1.2.0")
```
---
## Initialize DevKit
Call this **after** `NativeblocksManager.initialize`. Always restrict to debug builds.
```kotlin:MainActivity.kt showLineNumbers
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NativeblocksManager.initialize(
applicationContext = this,
edition = NativeblocksEdition.Cloud(
endpoint = NATIVEBLOCKS_API_URL,
apiKey = NATIVEBLOCKS_API_KEY,
developmentMode = true
)
)
if (BuildConfig.DEBUG) {
val devKit = DevKit.Builder(this)
.keepScreenOn() // keeps screen on during live sessions
.autoConnect() // connects to the CLI dev server automatically
.logTracking() // streams all SDK events to the CLI console
.build()
NativeblocksManager.getInstance().wandkit(devKit)
}
}
```
```swift showLineNumbers
@main
struct SampleApp: App {
init() {
NativeblocksManager.initialize(
edition: .cloud(
endpoint: NATIVEBLOCKS_API_URL,
apiKey: NATIVEBLOCKS_API_KEY,
developmentMode: true
)
)
#if DEBUG
let devKit = DevKit.Builder()
.keepScreenOn() // keeps screen on during live sessions
.autoConnect() // connects to the CLI dev server automatically
.logTracking() // streams all SDK events to the CLI console
.build()
NativeblocksManager.getInstance().wandKit(devKit)
#endif
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
```
---
## Builder options
| Method | What it does |
|--------|-------------|
| `keepScreenOn()` | Prevents screen from sleeping during a dev session |
| `autoConnect()` | Connects to the CLI dev server on the same network automatically |
| `logTracking()` | Pipes all `INativeLogger` events to the CLI log console |
---
## How hot reload works
1. Run the Nativeblocks CLI and connect to your device
2. Update a frame via the CLI (move a block, change a property, update a variable)
3. Deploy via the CLI
4. The frame updates in your running app in real time. No rebuild, no reinstall.
---
### [Foundation](https://nativeblocks.io/docs/sdk/foundation/)
Foundation is an **optional** package that provides ready-to-use blocks (text, button, image, layout) and type converters. Use it for demos, prototypes, and showcases. In production, bring your own design system components via `@NativeBlock` annotations.
---
## Add the Dependency
```groovy:build.gradle showLineNumbers
dependencies {
implementation("io.nativeblocks:nativeblocks-foundation-android:1.3.0")
}
```
```swift showLineNumbers
.package(
url: "https://github.com/nativeblocks/nativeblocks-foundation-ios",
.upToNextMajor(from: "1.3.0")
)
```
---
## Register
Call `FoundationProvider.provide()` after `NativeblocksManager.initialize`:
```kotlin showLineNumbers
NativeblocksManager.initialize(...)
FoundationProvider.provide()
```
```swift showLineNumbers
import NativeblocksFoundation
NativeblocksManager.initialize(...)
FoundationProvider.provide()
```
---
## Provided blocks
| Block | Description |
|-------|-------------|
| `NativeBox` | Box layout container |
| `NativeColumn` | Vertical column |
| `NativeRow` | Horizontal row |
| `NativeLazyColumn` | Scrollable vertical list |
| `NativeLazyRow` | Scrollable horizontal list |
| `NativeSpacer` | Flexible spacer |
| `NativeText` | Text display |
| `NativeTextField` | Text input |
| `NativeButton` | Button |
| `NativeImage` | Image |
| Block | Description |
|-------|-------------|
| `NativeZStack` | Z-axis stack |
| `NativeHStack` | Horizontal stack |
| `NativeVStack` | Vertical stack |
| `NativeLazyHStack` | Scrollable horizontal list |
| `NativeLazyVStack` | Scrollable vertical list |
| `NativeSpacer` | Flexible spacer |
| `NativeText` | Text display |
| `NativeTextField` | Text input |
| `NativeButton` | Button |
| `NativeImage` | Image |
| `NativeScrollView` | Scrollable container |
---
## Provided type converters
Foundation also registers type converters so Nativeblocks can serialize/deserialize platform-specific types from frame JSON.
| Type | Description |
|------|-------------|
| `Dp` | Density-independent pixel unit |
| `Color` | Compose color |
| `LayoutDirection` | LTR / RTL layout |
| `TextUnit` | SP text size |
| `Alignment` | Box alignment |
| `FontWeight` | Font weight |
| `TextAlign` | Text alignment |
| `ContentScale` | Image content scale |
| `Arrangement.Horizontal` | Horizontal arrangement |
| `Arrangement.Vertical` | Vertical arrangement |
| `TextOverflow` | Text overflow behaviour |
| `KeyboardType` | Keyboard input type |
| Type | Description |
|------|-------------|
| `Color` | SwiftUI color |
| `Font.Design` | Font design |
| `Font.Weight` | Font weight |
| `HorizontalAlignment` | Horizontal alignment |
| `VerticalAlignment` | Vertical alignment |
| `LayoutDirection` | LTR / RTL layout |
| `Axis.Set` | Scroll axis |
| `TextAlignment` | Text alignment |
---
### [Overview](https://nativeblocks.io/docs/cli/overview/)
The Nativeblocks CLI is the primary tool for working with frames. You use it to set up your project, register blocks and actions, write and deploy frames, and manage releases.
---
## What you can do
| Task | Command |
|------|---------|
| **Set up a project** | `nativeblocks init`: authenticate, select org and project, scaffold frame files |
| **Sync integration** | `nativeblocks integration sync`: push a generated schema to the server |
| **Generate the typed DSL** | `nativeblocks code-gen`: pull integrations and generate typed frame builder functions |
| **Develop with hot reload** | `nativeblocks frame deploy --watch`: re-deploy on every save, push to connected devices |
| **Ship to production** | `nativeblocks frame deploy --tag`: create a versioned release for the Dashboard |
| **Automate in CI** | Token auth + `--id` flags + `--force`. No interactive prompts. |
---
## DSL languages
Frames are written in **Kotlin**, **TypeScript**, or **Swift**. All three languages compile to the same frame definition at deploy time.
| | Kotlin | TypeScript | Swift |
|-|--------|------------|-------|
| File extension | `.kt` | `.ts` | `.swift` |
| Entry point | `fun frame(): NativeblocksFrame` | `function frame(): NativeblocksFrame` | `func frame() -> any NativeblocksFrame` |
| Best for | Android teams already on Kotlin | Web or cross-platform teams | iOS teams already on Swift |
---
## Typical workflow
```bash
# 1. First-time setup
nativeblocks auth login
nativeblocks organization set
nativeblocks project set
nativeblocks init
# 2. After annotating a new block or action
nativeblocks code-gen
# 3. During development
nativeblocks frame deploy -f src/home.ts --watch
# 4. Shipping to production
nativeblocks frame deploy -f src/home.ts --tag v1.2.0 --force
```
---
## Guides
| Guide | When to use |
|-------|-------------|
| [First-time Setup](/docs/cli/setup/) | Starting a new project |
| [Sync integration](/docs/cli/sync-integration/) | After annotating a new component |
| [Develop with Hot Reload](/docs/cli/develop/) | During active frame development |
| [Deploy to Production](/docs/cli/deploy/) | Shipping a tagged release |
| [CI/CD Integration](/docs/cli/ci/) | Automating deploys on push or merge |
---
## DSL reference
| Reference | What it covers |
|-----------|---------------|
| [Frame](/docs/reference/frame/) | Entry point, platform factories, lifecycle hooks |
| [Variable](/docs/reference/variable/) | Types, `remember()`, runtime conversion |
| [Block](/docs/reference/block/) | Builder pattern, properties, data, slots, imports |
| [Action](/docs/reference/action/) | `script()`, chaining, `updateBlockProperties` |
---
### [Command Reference](https://nativeblocks.io/docs/cli/)
### Installation
```bash
curl -fsSL https://nativeblocks.io/download/cli/installer.sh | bash
```
You can always find all commands with the help command:
```bash
nativeblocks help
```
---
### Init
Initialize a new Nativeblocks project scaffold. Authenticates, sets up organization and project, pulls installed integrations, and creates sample frames.
```bash
nativeblocks init
```
---
### Auth
Multiple accounts are supported (e.g. personal and company). Each login adds an account; tokens are kept per email. Every project remembers which account it uses. With a single account it is used automatically; with several, pick one per project via `nativeblocks auth switch`.
#### Log in with email OTP
```bash
nativeblocks auth login
```
#### Authenticate with a JWT access token
```bash
nativeblocks auth token "your.jwt.token"
```
#### List accounts
```bash
nativeblocks auth list
```
#### Switch account
Pins an account to the current project (run inside an initialized project).
| Flag | Description |
|------|-------------|
| `--email` | Account email. skips interactive selection (for CI/CD) |
```bash
nativeblocks auth switch
nativeblocks auth switch --email "you@company.com"
```
#### Log out
| Flag | Description |
|------|-------------|
| `--email` | Account email. skips interactive selection |
```bash
nativeblocks auth logout
```
#### Export compiler config
Write `nativeblocks.json` (endpoint, auth token, and organization) for the Android/iOS compiler plugin. This file contains your auth token — keep it out of version control.
| Flag | Short | Description |
|------|-------|-------------|
| `--directory` | `-d` | Directory to write `nativeblocks.json` into (defaults to `.`) |
```bash
nativeblocks auth compiler
nativeblocks auth compiler -d /path/to/dir
```
---
### Organization
#### Create organization
| Flag | Description |
|------|-------------|
| `--name` | Organization name |
```bash
nativeblocks organization create --name "My Organization"
```
#### Select organization
| Flag | Description |
|------|-------------|
| `--id` | Organization ID. skips interactive selection (for CI/CD) |
```bash
nativeblocks organization set
nativeblocks organization set --id "your-org-id"
```
#### Show current organization
```bash
nativeblocks organization get
```
---
### Project
#### Create project
| Flag | Description |
|------|-------------|
| `--name` | Project name |
```bash
nativeblocks project create --name "My Project"
```
#### Select project
Saves the selection to `.nativeblocks/` in the current directory.
| Flag | Description |
|------|-------------|
| `--id` | Project ID. skips interactive selection (for CI/CD) |
```bash
nativeblocks project set
nativeblocks project set --id "your-project-id"
```
#### Show current project
```bash
nativeblocks project get
```
---
### Integration
#### List integrations
Both flags are required.
| Flag | Short | Description |
|------|-------|-------------|
| `--platform` | `-p` | Platform support filter (e.g. `ANDROID`, `IOS`) |
| `--kind` | `-k` | Integration kind (`BLOCK` or `ACTION`) |
```bash
nativeblocks integration list -p ANDROID -k BLOCK
```
#### Sync integration
Upload a local integration to the server.
| Flag | Short | Description |
|------|-------|-------------|
| `--directory` | `-d` | Directory containing `integration.json` |
```bash
nativeblocks integration sync -d /path/to/integration
```
#### Get integration
Download an integration from the server to a local directory.
| Flag | Short | Description |
|------|-------|-------------|
| `--integrationId` | `-i` | Integration ID |
| `--directory` | `-d` | Directory to save integration files |
```bash
nativeblocks integration get -i "2222-2222-2222-2222" -d /path/to/save
```
---
### Frame
#### Create frame
Generate a placeholder frame file for the given name and platform. Reads the project language automatically from `.nativeblocks/`.
| Flag | Short | Description |
|------|-------|-------------|
| `--name` | `-n` | Frame name (e.g. `HomeScreen`) |
| `--platform` | `-p` | Target platform: `ANDROID` or `IOS` |
```bash
nativeblocks frame create --name HomeScreen --platform ANDROID
nativeblocks frame create -n Dashboard -p IOS
```
#### Deploy frame
Build a frame file (TypeScript, Kotlin, or Swift) and deploy it to the backend.
| Flag | Short | Description |
|------|-------|-------------|
| `--file` | `-f` | Frame file to deploy |
| `--watch` | | Watch for changes, re-deploy, and trigger hot-reload on connected devices |
| `--log` | | Stream logs from connected SDK devices (requires `--watch`) |
| `--tag` | `-t` | Mark this deployment as a named version for release management (e.g. `v1.0.0`). Cannot be combined with `--watch`. |
| `--force` | | Skip confirmation prompts. for CI/CD pipelines |
```bash
nativeblocks frame deploy -f ./src/welcome-android.ts
nativeblocks frame deploy -f ./src/welcome-android.kt --watch
nativeblocks frame deploy -f ./src/welcome-ios.swift --watch
nativeblocks frame deploy -f ./src/welcome-android.ts --watch --log
nativeblocks frame deploy -f ./src/welcome-android.ts --tag v1.0.0
nativeblocks frame deploy -f ./src/welcome-android.ts --tag v1.0.0 --force
```
---
### Code Gen
Generate typed block and action classes from the installed integrations. Reads the language and options configured by `nativeblocks init`.
```bash
nativeblocks code-gen
```
---
### AI
#### List skills
List available AI skills and supported agents.
```bash
nativeblocks ai skill list
```
#### Add skill
Install Nativeblocks skill files for your AI coding tool.
```bash
nativeblocks ai skill add
```
---
### Update
Update the CLI to the latest version. Downloads the official installer and runs it, replacing the current binary in place.
```bash
nativeblocks update
```
---
### [First-time Setup](https://nativeblocks.io/docs/cli/setup/)
This walks you through getting from zero to a working project. Run this once per project.
---
## 1. Install the CLI
```bash
curl -fsSL https://nativeblocks.io/download/cli/installer.sh | bash
```
Verify it works:
```bash
nativeblocks help
```
---
## 2. Log in
```bash
nativeblocks auth login
```
This sends a one-time code to your email. Paste it in when prompted. Your credentials are stored locally so you stay logged in between sessions.
You can log in with multiple accounts (e.g. personal and company) — each login adds an account and tokens are kept per email. With a single account it is used automatically; with several, pin one to the current project with `nativeblocks auth switch`. See [Command Reference](/docs/cli/) for `auth list`, `switch`, and `logout`.
---
## 3. Set up your organization
If you are creating a new organization:
```bash
nativeblocks organization create --name "Acme Corp"
```
If your organization already exists, select it:
```bash
nativeblocks organization set
```
---
## 4. Set up your project
Create a new project:
```bash
nativeblocks project create --name "My App"
```
Or select an existing one:
```bash
nativeblocks project set
```
The selected project is saved to `.nativeblocks/` in your current directory. Run `project set` from the repo root so every command in that directory targets the right project.
---
## 5. Initialize
```bash
nativeblocks init
```
This pulls your integrations from the server, generates the typed DSL, and creates starter frame files. After it finishes you have everything you need to start writing and deploying frames.
---
## What's next
- [Sync integration](/docs/cli/sync-integration/) after annotating new components
- [Develop a frame with hot reload](/docs/cli/develop/) to iterate quickly
- [Deploy to production](/docs/cli/deploy/) when you are ready to ship
---
### [Sync integration](https://nativeblocks.io/docs/cli/sync-integration/)
After your blocks or actions are registered on the server, run `code-gen` to pull the latest integrations and regenerate the typed DSL functions used in your frame files.
---
## Command
```bash
nativeblocks code-gen
```
---
## When to run it
Run `code-gen` whenever the integrations on the server change:
- After a new block or action is registered for the first time
- After an existing block or action is updated (renamed property, new data, etc.)
- After removing a block or action from the server
---
## What it does
`code-gen` fetches your current integrations from the server and writes a typed DSL file into your project. Each registered block or action gets a corresponding function that your frame files can call with full type safety.
---
## Verify
List your integrations to confirm the expected blocks and actions are present:
```bash
nativeblocks integration list -p ANDROID -k BLOCK
nativeblocks integration list -p IOS -k ACTION
```
---
## What's next
- [Develop a frame with hot reload](/docs/cli/develop/) to use the generated DSL in a frame
---
### [Develop with Hot Reload](https://nativeblocks.io/docs/cli/develop/)
The `--watch` flag keeps the CLI running and re-deploys your frame every time you save the file. Combined with DevKit in your app, changes appear on your device without a rebuild. That is hot reload.
---
## Prerequisites
- DevKit set up in your app. See [Hot Reload](/docs/sdk/hot-reload/) for setup.
- Your device or emulator running the app in debug mode with DevKit connected.
---
## Start watching
```bash
nativeblocks frame deploy -f ./frames/home.ts --watch
```
The CLI deploys the frame once immediately, then watches for file changes. On every save it re-deploys and sends a signal to DevKit, which refreshes the frame on your device.
---
## Typical development loop
1. Run `frame deploy --watch` in one terminal
2. Open the frame file in your editor
3. Make a change and save
4. The frame updates on your device in seconds
No app rebuild. No app restart.
---
## Stream device logs
Add `--log` to stream logs from connected SDK devices while you watch. It requires `--watch`.
```bash
nativeblocks frame deploy -f ./frames/home.ts --watch --log
```
This is useful for catching action failures and variable issues without leaving the terminal.
---
## Notes
- `--watch` and `--tag` cannot be combined. Tags are for production releases, not live development sessions.
- If the CLI loses the connection to DevKit, restart the watch command.
- Multiple frame files need separate `--watch` processes, one per file.
---
## What's next
- [Deploy to production](/docs/cli/deploy/) when the frame is ready to ship
---
### [Deploy to Production](https://nativeblocks.io/docs/cli/deploy/)
When a frame is ready to ship, deploy it with a tag. Tags are what the Dashboard tracks for release management, percentage rollouts, and rollbacks.
---
## Deploy with a tag
```bash
nativeblocks frame deploy -f ./frames/home.ts --tag v1.2.0
```
The frame is uploaded and a new tagged version is created. It is not yet active. Users still see the previously active version.
---
## Activate in the Studio
Go to the Studio and open your frame's release management panel. From there you can:
- **Set Direct release** for the new tag to make it live for all users
- **Set a percentage release** to gradually release to a subset of users
- **Set a conditional release** to deliver the frame to a subset of users
- **Roll back** to any previous tag with one click
---
## Tagging strategy
Use a consistent versioning scheme that matches your app releases. Some teams align CLI tags with their app version codes, others use independent semantic versions.
```bash
# Align with app version
nativeblocks frame deploy -f ./frames/home.ts --tag v2.5.0
# Independent frame versioning
nativeblocks frame deploy -f ./frames/home.ts --tag home-v3
```
---
## Deploy multiple frames
Deploy each frame separately. You can tag them all with the same version string to group them as a release:
```bash
nativeblocks frame deploy -f ./frames/home.ts --tag v1.2.0
nativeblocks frame deploy -f ./frames/profile.ts --tag v1.2.0
nativeblocks frame deploy -f ./frames/checkout.ts --tag v1.2.0
```
---
## What's next
- [CI/CD integration](/docs/cli/ci/) to automate deploys on merge or push
---
### [CI/CD Integration](https://nativeblocks.io/docs/cli/ci/)
In CI environments you cannot use interactive prompts. Use a service account token for auth, pass org and project IDs directly, and add `--force` to skip any confirmation prompts.
---
## Full pipeline
```bash
nativeblocks auth token "$NB_TOKEN"
nativeblocks organization set --id $NB_ORG_ID
nativeblocks project set --id $NB_PROJECT_ID
nativeblocks frame deploy -f src/home.ts --tag $VERSION --force
```
- `auth token` takes the JWT as a positional argument
- `--id` bypasses interactive org/project selection
- `--force` skips tag confirmation and ownership transfer prompts
Store `NB_TOKEN`, `NB_ORG_ID`, and `NB_PROJECT_ID` as secrets in your CI provider. Never hardcode them.
---
## GitHub Actions example
```yaml
name: Deploy frames
on:
push:
tags:
- "v*"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Nativeblocks CLI
run: curl -fsSL https://nativeblocks.io/download/cli/installer.sh | bash
- name: Deploy frames
env:
NB_TOKEN: ${{ secrets.NB_TOKEN }}
NB_ORG_ID: ${{ secrets.NB_ORG_ID }}
NB_PROJECT_ID: ${{ secrets.NB_PROJECT_ID }}
run: |
nativeblocks auth token "$NB_TOKEN"
nativeblocks organization set --id $NB_ORG_ID
nativeblocks project set --id $NB_PROJECT_ID
nativeblocks frame deploy -f src/home.ts --tag ${{ github.ref_name }} --force
nativeblocks frame deploy -f src/profile.ts --tag ${{ github.ref_name }} --force
nativeblocks frame deploy -f src/checkout.ts --tag ${{ github.ref_name }} --force
```
---
## Notes
- Frames deployed in CI are not yet active. Go to the Dashboard to activate a tag, set a rollout percentage, or roll back.
- Each `frame deploy --tag` call is independent. If one fails, the others are not affected.
- Set `$VERSION` from your pipeline's git tag, build number, or commit SHA.
---
### [Frame](https://nativeblocks.io/docs/reference/frame/)
A frame is a single function that returns a `NativeblocksFrame`. The CLI locates it by the fixed name `frame()`. One frame function per file.
---
## Entry point
```kotlin showLineNumbers
fun frame(): NativeblocksFrame = androidFrame(name = "name", route = "/route") {
// variables, lifecycle, rootBlock
}
```
```tsx showLineNumbers
function frame(): NativeblocksFrame {
const frame = androidFrame({ name: "name", route: "/route" });
// variables, lifecycle
return frame.rootBlock(/* JSX */);
}
```
```swift showLineNumbers
func frame() -> any NativeblocksFrame {
androidFrame(name: "name", route: "/route") { f in
// variables, lifecycle, f.rootBlock
}
}
```
The frame file extension is `.kt` (Kotlin), `.tsx` (TypeScript), or `.swift` (Swift). Never add an entry point (`main`, `@main`), the CLI injects its own runner at deploy time.
---
## Platform factories
| Platform | Kotlin | TypeScript | Swift |
|----------|--------|------------|-------|
| Android | `androidFrame(name, route, type) { }` | `androidFrame({ name, route, type })` | `androidFrame(name:route:type:) { f in }` |
| iOS | `iosFrame(name, route, type) { }` | `iosFrame({ name, route, type })` | `iosFrame(name:route:type:) { f in }` |
- `name`: PascalCase. For example: `"ProductDetail"`, `"CheckoutFlow"`
- `route`: kebab-case path. For example: `"/product-detail"`, `"/order/{orderId}"`
- `type`: optional `FrameType`, defaults to `FRAME`. See [Frame type](#frame-type).
The platform factory ties the frame to a platform, so its `rootBlock` and slots accept only that platform's blocks.
---
## Frame type
A frame is presented as a full screen, a bottom sheet, or a dialog. The `type` parameter selects how the SDK renders it; it defaults to `FRAME` when omitted.
| Value | Renders as |
|-------|------------|
| `FRAME` | A full-screen frame (default) |
| `BOTTOM_SHEET` | A bottom sheet that slides up over the current screen |
| `DIALOG` | A modal dialog centered over the current screen |
```kotlin showLineNumbers
// Full screen (default)
androidFrame(name = "Home", route = "/home") { /* ... */ }
// Bottom sheet
androidFrame(name = "Filters", route = "/filters", type = FrameType.BOTTOM_SHEET) { /* ... */ }
// Dialog
androidFrame(name = "Confirm", route = "/confirm", type = FrameType.DIALOG) { /* ... */ }
```
```tsx showLineNumbers
// Full screen (default)
androidFrame({ name: "Home", route: "/home" });
// Bottom sheet
androidFrame({ name: "Filters", route: "/filters", type: "BOTTOM_SHEET" });
// Dialog
androidFrame({ name: "Confirm", route: "/confirm", type: "DIALOG" });
```
```swift showLineNumbers
// Full screen (default)
iosFrame(name: "Home", route: "/home") { f in /* ... */ }
// Bottom sheet
iosFrame(name: "Filters", route: "/filters", type: .bottomSheet) { f in /* ... */ }
// Dialog
iosFrame(name: "Confirm", route: "/confirm", type: .dialog) { f in /* ... */ }
```
---
## Lifecycle hooks
`onAppear` runs when the frame mounts; `onDisappear` runs when it unmounts. Both are optional and take actions.
```kotlin showLineNumbers
androidFrame(name = "Home", route = "/home") {
onAppear {
script { _, updateVariable, _ -> updateVariable(loadingVar, "true") }
}
onDisappear {
script { _, updateVariable, _ -> updateVariable(loadingVar, "false") }
}
rootBlock { /* ... */ }
}
```
```tsx showLineNumbers
return frame
.onAppear(
script((_, updateVariable) => { updateVariable(loadingVar, "true"); })
)
.onDisappear(
script((_, updateVariable) => { updateVariable(loadingVar, "false"); })
)
.rootBlock(/* JSX */);
```
```swift showLineNumbers
iosFrame(name: "Home", route: "/home") { f in
f.onAppear {
script { _, updateVariable, _ in
updateVariable(loadingVar, "true")
}
}
.onDisappear {
script { _, updateVariable, _ in
updateVariable(loadingVar, "false")
}
}
.rootBlock { /* ... */ }
}
```
---
## Full example
```kotlin:profile-android.kt showLineNumbers
package com.example.app.frames
import com.example.app.generated.shared.*
import com.example.app.generated.integration.android.Script.script
import com.example.app.generated.integration.android.nativeblocks.button.nativeblocksButton
import com.example.app.generated.integration.android.nativeblocks.column.NativeblocksColumn
import com.example.app.generated.integration.android.nativeblocks.column.nativeblocksColumn
import com.example.app.generated.integration.android.nativeblocks.image.NativeblocksImage
import com.example.app.generated.integration.android.nativeblocks.image.nativeblocksImage
import com.example.app.generated.integration.android.nativeblocks.spacer.nativeblocksSpacer
import com.example.app.generated.integration.android.nativeblocks.text.nativeblocksText
fun frame(): NativeblocksFrame = androidFrame(name = "Profile", route = "/profile") {
val avatarUrl by remember("https://example.com/avatar.jpg")
val username by remember("John Doe")
val bio by remember("Mobile engineer")
val isFollowing by remember(false)
val followLabel by remember("Follow")
onAppear {
script { _, updateVariable, _ ->
updateVariable(followLabel, "Follow")
}
}
rootBlock {
nativeblocksColumn(
blockKey = "main",
width = NativeblocksColumn.WidthOptions.Match,
height = NativeblocksColumn.HeightOptions.Match,
horizontalAlignment = NativeblocksColumn.HorizontalAlignmentOptions.Center,
content = {
nativeblocksSpacer(blockKey = "topSpacer")
nativeblocksImage(
blockKey = "avatar",
imageUrl = avatarUrl,
width = NativeblocksImage.WidthOptions.Wrap,
scaleType = NativeblocksImage.ScaleTypeOptions.Fit,
)
nativeblocksText(blockKey = "nameText", text = username, fontSize = "20")
nativeblocksText(blockKey = "bioText", text = bio, fontSize = "14")
nativeblocksSpacer(blockKey = "gap")
nativeblocksButton(
blockKey = "followButton",
label = followLabel,
onClick = {
script { getVariable, _, _ ->
"""
let f = ${getVariable(isFollowing)} !== "true";
updateVariable("${isFollowing.key}", String(f));
updateVariable("${followLabel.key}", f ? "Unfollow" : "Follow");
""".trimIndent()
}
},
)
nativeblocksSpacer(blockKey = "bottomSpacer")
},
)
}
}
```
```tsx:profile-android.tsx showLineNumbers
import { androidFrame, NativeblocksFrame, remember } from "@nativeblocks";
import { script } from "@nativeblocks/android/Script/Script";
import { NativeblocksButton } from "@nativeblocks/android/nativeblocks/button/NativeblocksButton";
import { NativeblocksColumn } from "@nativeblocks/android/nativeblocks/column/NativeblocksColumn";
import { NativeblocksImage } from "@nativeblocks/android/nativeblocks/image/NativeblocksImage";
import { NativeblocksSpacer } from "@nativeblocks/android/nativeblocks/spacer/NativeblocksSpacer";
import { NativeblocksText } from "@nativeblocks/android/nativeblocks/text/NativeblocksText";
function frame(): NativeblocksFrame {
const frame = androidFrame({ name: "Profile", route: "/profile" });
const avatarUrl = remember("https://example.com/avatar.jpg");
const username = remember("John Doe");
const bio = remember("Mobile engineer");
const isFollowing = remember(false);
const followLabel = remember("Follow");
return frame
.onAppear(
script((_, updateVariable) => { updateVariable(followLabel, "Follow"); })
)
.rootBlock(
{
const f = getVariable(isFollowing) !== "true";
updateVariable(isFollowing, String(f));
updateVariable(followLabel, f ? "Unfollow" : "Follow");
})}
/>
);
}
```
```swift:ProfileIOS.swift showLineNumbers
import NativeblocksIOS
func frame() -> any NativeblocksFrame {
iosFrame(name: "Profile", route: "/profile") { f in
let avatarUrl = remember("https://example.com/avatar.jpg")
let username = remember("John Doe")
let bio = remember("Mobile engineer")
let isFollowing = remember(false)
let followLabel = remember("Follow")
f.onAppear {
script { _, updateVariable, _ in
updateVariable(followLabel, "Follow")
}
}
.rootBlock {
NativeblocksVstack(blockKey: "main") {
NativeblocksSpacer(blockKey: "topSpacer")
NativeblocksImage(blockKey: "avatar", imageUrl: avatarUrl)
.width("auto")
.contentMode("fit")
NativeblocksText(blockKey: "nameText", text: username)
.fontSize("20")
NativeblocksText(blockKey: "bioText", text: bio)
.fontSize("14")
NativeblocksSpacer(blockKey: "gap")
NativeblocksButton(blockKey: "followButton", label: followLabel)
.onClick {
script { getVariable, _, _ in
#"""
let f = \#(getVariable(isFollowing)) !== "true";
updateVariable("\#(isFollowing.key)", String(f));
updateVariable("\#(followLabel.key)", f ? "Unfollow" : "Follow");
"""#
}
}
NativeblocksSpacer(blockKey: "bottomSpacer")
}
.width("fill")
.height("fill")
.alignmentHorizontal("center")
}
}
}
```
---
## Deploy
```bash
# Development
nativeblocks frame deploy --file src/profile-android.kt --watch
# Production
nativeblocks frame deploy --file src/profile-android.kt --tag v1.0.0
```
```bash
# Development
nativeblocks frame deploy --file src/profile-android.tsx --watch
# Production
nativeblocks frame deploy --file src/profile-android.tsx --tag v1.0.0
```
```bash
# Development
nativeblocks frame deploy --file src/ProfileIOS.swift --watch
# Production
nativeblocks frame deploy --file src/ProfileIOS.swift --tag v1.0.0
```
`--tag` marks the deployed frame as a named release version; `--tag` and `--watch` are mutually exclusive.
---
### [Variable](https://nativeblocks.io/docs/reference/variable/)
Variables are the only way to carry dynamic values in a frame. You declare one with `remember(value)`. The binding name becomes the variable key, the key you use to read and write the value at runtime. There is no type annotation and no separate string key to maintain.
---
## Types
Every variable resolves to one of six wire types. You never write the type, it is inferred from the value you pass to `remember()`.
| Type | Use for |
|------|---------|
| `STRING` | Any text, URLs, JSON arrays/objects |
| `INT` | Integer number |
| `LONG` | Large integer |
| `DOUBLE` | Decimal number |
| `FLOAT` | Float number |
| `BOOLEAN` | `true` / `false`. Also controls block visibility. |
All variable values are stored and passed as **strings** at runtime. Always convert explicitly inside scripts: `Number()` / `String()` (the runtime is JavaScript).
---
## Type inference
`remember(value)` infers the wire type from the value's native type:
| Wire type | Kotlin | Swift | TypeScript |
|-----------|--------|-------|------------|
| `STRING` | `String` | `String` | `string` |
| `INT` | `Int` | `Int` | integer `number` |
| `LONG` | `Long` | `Int64` | `bigint` |
| `DOUBLE` | `Double` | `Double` | non-integer `number` |
| `FLOAT` | `Float` | `Float` | use a serializer |
| `BOOLEAN` | `Boolean` | `Bool` | `boolean` |
A TypeScript `number` cannot distinguish `LONG`/`FLOAT`: an integer becomes `INT`, a non-integer becomes `DOUBLE`. Use a `bigint` for `LONG`.
---
## Declaration
Declare variables inside the frame lambda, before `rootBlock { }`. Use `by remember(...)` and no type annotation.
```kotlin showLineNumbers
val title by remember("default") // STRING
val count by remember(0) // INT
val big by remember(0L) // LONG
val ratio by remember(0.0) // DOUBLE
val weight by remember(0.0f) // FLOAT
val visible by remember(true) // BOOLEAN
```
Declare variables between `const frame = ...` and `return frame.rootBlock(...)`. Use `remember(...)` and no type annotation.
```tsx showLineNumbers
const title = remember("default"); // STRING
const count = remember(0); // INT (integer)
const ratio = remember(1.5); // DOUBLE (non-integer)
const big = remember(10n); // LONG (bigint)
const visible = remember(true); // BOOLEAN
```
Declare variables inside the frame closure, before `f.rootBlock { }`. Use `remember(...)` and no type annotation.
```swift showLineNumbers
let title = remember("default") // STRING
let count = remember(0) // INT
let big = remember(Int64(0)) // LONG
let ratio = remember(0.0) // DOUBLE
let weight = remember(Float(0)) // FLOAT
let visible = remember(true) // BOOLEAN
```
---
## Custom variable types
`remember` accepts any value, not just primitives. To store a custom type, write a **`VariableSerializer`**: it maps your value to one of the six wire types plus a string. Register it once in the project bootstrap, then pass instances of your type straight to `remember()`. The CLI resolves them to `(wire type, string)` at frame-build time, the wire type never carries your custom class.
The bootstrap is a conventional file named `Nativeblocks` that exposes a `setup()` method. The CLI instantiates it and calls `setup()` once before building any frame, so you never register serializers manually inside a frame.
Define the type and its serializer in their own file, then register it in `src//Nativeblocks.kt` (the root of your base package, the only place the CLI looks).
```kotlin showLineNumbers
// model/User.kt - its own file, never inside a frame
data class User(val name: String, val age: Int)
class UserSerializer : VariableSerializer {
override val type = VariableType.STRING
override fun serialize(value: User): String = "${value.name} (${value.age})"
}
```
```kotlin showLineNumbers
// src/com/example/app/Nativeblocks.kt - the project bootstrap
package com.example.app
import com.example.app.generated.shared.VariableSerializers
import com.example.app.model.UserSerializer
class Nativeblocks {
fun setup() {
VariableSerializers.register(UserSerializer())
}
}
```
```kotlin showLineNumbers
// any frame - import the type, never declare it in the frame
import com.example.app.model.User
val user by remember(User("Alex", 30)) // resolves to STRING "Alex (30)" at build time
```
> The runner inlines the frame body, so a type declared **inside** a frame loses its package and won't match the serializer registered against its real package (you get `No VariableSerializer registered for X` at build time). Always define custom types in their own file and `import` them.
Define the type and serializer (the `target` field is the class constructor, since TypeScript erases generics), then register it in `src/Nativeblocks.ts` (the root of `src/`, the only place the CLI looks).
```typescript showLineNumbers
// src/Nativeblocks.ts - the project bootstrap
import { register, VariableSerializer } from "@nativeblocks";
export class User {
constructor(public name: string, public age: number) {}
}
class UserSerializer implements VariableSerializer {
readonly target = User;
readonly type = "STRING" as const;
serialize(v: User): string { return `${v.name} (${v.age})`; }
}
export class Nativeblocks {
setup() {
register(new UserSerializer());
}
}
```
```tsx showLineNumbers
// any frame
import { User } from "./Nativeblocks";
const user = remember(new User("Alex", 30)); // resolves to STRING "Alex (30)" at build time
```
Define the type and serializer in their own file, then register it in `src/Nativeblocks.swift` (the root of `src/`, the only place the CLI looks).
```swift showLineNumbers
// User.swift - its own file, never inside a frame
import NativeblocksShared
struct User { let name: String; let age: Int }
struct UserSerializer: VariableSerializer {
var type: VariableType { .STRING }
func serialize(_ value: User) -> String { "\(value.name) (\(value.age))" }
}
```
```swift showLineNumbers
// src/Nativeblocks.swift - the project bootstrap
import NativeblocksShared
final class Nativeblocks {
func setup() {
VariableSerializers.register(UserSerializer())
}
}
```
```swift showLineNumbers
// any frame
let user = remember(User(name: "Alex", age: 30)) // resolves to STRING "Alex (30)"
```
> Custom types must live in a non-frame file. The CLI only stages non-frame `.swift` files, so a type declared inside a frame won't be visible at build time.
---
## Rules
- `remember(value)` takes a single value and **no type annotation**. The key is the binding name, never written by hand.
- Use `STRING` for JSON arrays and objects, serialize with `JSON.stringify()` / `JSON.parse()` inside scripts.
- `BOOLEAN` variables can be bound to a block's `blockVisibilityKey` to control show/hide.
- Variable keys must be unique within a frame.
- Never hardcode user-facing text, always use a variable.
- For any type that is not a built-in primitive, write a `VariableSerializer`. Never register one manually inside a frame, register it in the `Nativeblocks` bootstrap.
---
## Runtime conversion examples
Scripts run as JavaScript at runtime. Read a value with the `getVariable` helper (it produces the `getVariable("key")` call) and convert it explicitly. See [Action](/docs/reference/action/) for the full `script` reference.
The `script` build closure returns the JavaScript body string. `getVariable(v)` splices in the read expression; `v.key` gives the variable's key.
```kotlin showLineNumbers
// Numeric
script { getVariable, _, _ ->
"""updateVariable("${count.key}", String(Number(${getVariable(count)}) + 1));"""
}
// Boolean toggle
script { getVariable, _, _ ->
"""updateVariable("${flag.key}", String(${getVariable(flag)} !== "true"));"""
}
// JSON list
script { getVariable, _, _ ->
"""
let list = JSON.parse(${getVariable(items)});
list.push({ id: "new" });
updateVariable("${items.key}", JSON.stringify(list));
""".trimIndent()
}
```
The `script` body is regular JavaScript. The CLI rewrites `getVariable(count)` / `updateVariable(count, ...)` into the string-keyed calls at build time.
```tsx showLineNumbers
// Numeric
script((getVariable, updateVariable) => {
const n = Number(getVariable(count));
updateVariable(count, String(n + 1));
});
// Boolean toggle
script((getVariable, updateVariable) => {
const on = getVariable(flag) === "true";
updateVariable(flag, String(!on));
});
// JSON list
script((getVariable, updateVariable) => {
const list = JSON.parse(getVariable(items));
list.push({ id: "new" });
updateVariable(items, JSON.stringify(list));
});
```
The `script` build closure returns the JavaScript body string. `getVariable(v)` splices in the read expression; `v.key` gives the variable's key.
```swift showLineNumbers
// Numeric
script { getVariable, _, _ in
#"updateVariable("\#(count.key)", String(Number(\#(getVariable(count))) + 1));"#
}
// Boolean toggle
script { getVariable, _, _ in
#"updateVariable("\#(flag.key)", String(\#(getVariable(flag)) !== "true"));"#
}
// JSON list
script { getVariable, _, _ in
#"""
let list = JSON.parse(\#(getVariable(items)));
list.push({ id: "new" });
updateVariable("\#(items.key)", JSON.stringify(list));
"""#
}
```
---
### [Block](https://nativeblocks.io/docs/reference/block/)
A block is a UI component generated from an installed integration. You configure it with a key and optional visibility, set properties, bind data variables, nest children in slots, and attach event handlers. The surface differs per language: Kotlin uses named-argument factory functions, Swift instantiates classes and chains setters, TypeScript uses JSX.
---
## Configuration
Everything is a **named argument** of the factory function. There is no method chaining.
```kotlin showLineNumbers
nativeblocksColumn(
blockKey = "myKey",
blockVisibilityKey = boolVar,
width = NativeblocksColumn.WidthOptions.Match, // picker property -> typed option
paddingTop = "16", // plain property -> String
text = titleVar, // data -> bind a variable
content = { // slot -> nest children
childBlock1()
childBlock2()
},
onClick = { // event -> attach actions
action1()
},
)
```
Every block is a JSX component. Identity, properties, data, events, and slots are all props.
```tsx showLineNumbers
bind a variable
onClick={action1} // event -> attach an action
>
{/* bare children -> the primary slot */}
```
Blocks are classes. Instantiate them directly. `blockKey`, `blockVisibilityKey`, and data fields are constructor arguments; properties and events are chained methods; a single slot is the trailing closure.
```swift showLineNumbers
NativeblocksColumn(blockKey: "myKey", blockVisibilityKey: boolVar, text: titleVar) {
childBlock1()
childBlock2()
}
.width("fill")
.paddingTop("16")
.onClick {
action1()
}
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `blockKey` | string | Unique identifier within the frame. Required for blocks referenced in `updateBlockProperties()`. Auto-generated if omitted. |
| `blockVisibilityKey` | BOOLEAN variable | Links show/hide state to a boolean variable. Auto-generated if omitted. |
---
## Properties
Properties support up to three breakpoints: mobile, tablet, desktop. Only mobile is mandatory; tablet and desktop fall back to the mobile value unless you override them.
Each property takes a **single value** applied to all breakpoints. Picker properties take a generated option; plain properties take a `String`.
```kotlin showLineNumbers
nativeblocksColumn(
blockKey = "root",
width = NativeblocksColumn.WidthOptions.Match, // picker option
paddingTop = "16", // plain String
)
```
Common dimension values: `"match"` fills the parent, `"wrap"` fits the content.
A bare value fills all three breakpoints; the object form overrides per size (`mobile` required, `tablet`/`desktop` fall back to `mobile`).
```tsx showLineNumbers
{/* all breakpoints */}
{/* per breakpoint */}
```
Common dimension values: `"match"` fills the parent, `"wrap"` fits the content.
Each property setter takes mobile, with optional tablet and desktop. Omitted breakpoints inherit the mobile value.
```swift showLineNumbers
.width("fill") // all breakpoints
.width("fill", "auto", "auto") // mobile, tablet, desktop
.width("fill", nil, "auto") // tablet falls back to mobile
```
Common dimension values: `"fill"` fills the parent, `"auto"` fits the content. (Android integrations use `"match"` / `"wrap"`.)
---
## Typed properties
Properties constrained to a picker also expose a type-safe form. Prefer it to catch invalid values at compile time; use the string form when the value is dynamic.
```kotlin showLineNumbers
// Typed option (validated at compile time)
nativeblocksColumn(blockKey = "root", width = NativeblocksColumn.WidthOptions.Match)
// Dynamic string (loose pickers only)
nativeblocksColumn(blockKey = "root", width = NativeblocksColumn.WidthOptions.Custom(dynamicValue))
```
Option naming: `{ClassName}.{PropertyKey}Options.{Value}`, e.g. `NativeblocksColumn.WidthOptions.Match`. Loose pickers add a `.Custom("…")` case for arbitrary strings.
Picker properties are typed as a string-literal union; pass the literal directly.
```tsx showLineNumbers
```
Union type naming: `{ClassName}{PropertyKey}Options`, e.g. `NativeblocksColumnWidthOptions`.
```swift showLineNumbers
// String form
NativeblocksVstack(blockKey: "root").width("fill")
// Typed enum (validated at compile time)
NativeblocksVstack(blockKey: "root").width(.fill)
```
Enum naming: `{ClassName}{PropertyKey}Options`, e.g. `NativeblocksVstackWidthOptions`.
---
## Data
Data fields bind a frame variable to the block. Always pass a variable, never a raw string.
Data is a named argument whose name is the data key.
```kotlin showLineNumbers
nativeblocksText(blockKey = "title", text = titleVar)
nativeblocksImage(blockKey = "logo", imageUrl = logoVar)
```
Data is an attribute whose name is the data key.
```tsx showLineNumbers
```
Data is a constructor argument whose label is the data key.
```swift showLineNumbers
NativeblocksText(blockKey: "title", text: titleVar)
NativeblocksImage(blockKey: "logo", imageUrl: logoVar)
```
---
## Slots
Slots are named child-block containers. Only layout and container blocks expose slots; leaf blocks like text and image have none.
Each slot is a named-argument lambda. Call child block factories directly inside, they auto-register to the slot.
```kotlin showLineNumbers
nativeblocksCard(
blockKey = "card",
content = {
nativeblocksText(blockKey = "title", text = titleVar)
},
leadingIcon = {
nativeblocksImage(blockKey = "icon", imageUrl = iconVar)
},
)
```
Bare children go into the block's **primary slot** (the one named `content`, or the sole slot). Blocks with multiple slots expose each extra slot as a compound subcomponent ``.
```tsx showLineNumbers
{/* single / primary slot - bare children */}
{/* multiple slots - named slot subcomponents */}
{/* ... */}
```
A block with a **single slot** takes its children as the trailing closure on the constructor. A block with **multiple slots** exposes each as its own chained method (`.header { }`, `.footer { }`). Check the generated class to see which slots it has.
```swift showLineNumbers
// single slot - trailing closure
NativeblocksVstack(blockKey: "list") {
NativeblocksText(blockKey: "a", text: a)
NativeblocksText(blockKey: "b", text: b)
}
// multiple slots - named slot methods
NativeblocksScaffold(blockKey: "screen")
.topBar {
NativeblocksAppBar(blockKey: "bar", title: title)
}
.content {
NativeblocksVstack(blockKey: "body") { /* ... */ }
}
```
---
## Events
Events attach actions that fire when the block raises them. Event names are defined by the block (e.g. `onClick`, `onLongClick`). Multiple actions in the same event run top to bottom.
Events are named-argument lambdas. Call action factories directly inside.
```kotlin showLineNumbers
nativeblocksButton(
blockKey = "btn",
onClick = {
navigateAction(actionName = "nav")
script { _, updateVariable, _ ->
updateVariable(loadingVar, "true")
}
},
onLongClick = {
feedbackAction(actionName = "haptic")
},
)
```
Event props take an action, an array of actions, or a scope function.
```tsx showLineNumbers
{ updateVariable(loadingVar, "true"); }),
]}
onLongClick={feedbackAction()}
/>
```
Events are chained methods taking a closure. Call action factories directly inside.
```swift showLineNumbers
NativeblocksButton(blockKey: "btn")
.onClick {
NavigateAction(actionName: "nav")
script(name: "load") { _, updateVariable, _ in
updateVariable(loadingVar, "true")
}
}
.onLongClick {
FeedbackAction(actionName: "haptic")
}
```
---
## Auto-visibility
Every block without a user-supplied `blockVisibilityKey` gets one auto-generated as `{key}_visibility`, and a matching `BOOLEAN` variable with value `"true"` is injected into the frame. Only set `blockVisibilityKey` explicitly when you need to control a block's visibility from a script.
---
## Imports
```kotlin showLineNumbers
import com.example.app.generated.shared.*
import com.example.app.generated.integration.android.nativeblocks.column.nativeblocksColumn
import com.example.app.generated.integration.android.nativeblocks.text.nativeblocksText
// iOS equivalents
import com.example.app.generated.integration.ios.nativeblocks.vstack.nativeblocksVstack
```
Import the generated shared package first (it brings in the NB types and the frame factories), then each block factory directly.
Pattern: `{basePackage}.generated.integration.{platform}.nativeblocks.{keyType}.{factoryName}`. Import the class as well (`…column.NativeblocksColumn`) when you use its typed property options.
```tsx showLineNumbers
import { NativeblocksColumn } from "@nativeblocks/android/nativeblocks/column/NativeblocksColumn";
import { NativeblocksText } from "@nativeblocks/android/nativeblocks/text/NativeblocksText";
// iOS equivalents
import { NativeblocksVstack } from "@nativeblocks/ios/nativeblocks/vstack/NativeblocksVstack";
```
Pattern: `@nativeblocks/{platform}/nativeblocks/{keyType}/{ClassName}`. The frame factory and `remember` come from `@nativeblocks`.
```swift showLineNumbers
import NativeblocksIOS // iOS blocks + iosFrame
import NativeblocksANDROID // Android blocks + androidFrame
```
All block classes for a platform are bundled into a single Swift package target. `NativeblocksShared` is re-exported by both, so you never import it separately.
### Naming conventions
Reference blocks by their **unversioned alias** (no version suffix). It always resolves to the latest installed version.
| API keyType | Class | Factory / component | Unversioned alias |
|-------------|-------|---------------------|-------------------|
| `nativeblocks/column v2` | `NativeblocksColumn2` | `nativeblocksColumn2` | `NativeblocksColumn` / `nativeblocksColumn` |
| `nativeblocks/box v3` | `NativeblocksBox3` | `nativeblocksBox3` | `NativeblocksBox` / `nativeblocksBox` |
Only use a versioned name if you explicitly need a specific version.
---
## Helper functions
When the same UI pattern repeats, extract it into a helper declared outside the frame function.
Helpers must extend `BlocksScope` so block factory calls self-register:
```kotlin showLineNumbers
fun BlocksScope.productRow(key: String, nameVar: Variable, priceVar: Variable) {
nativeblocksRow(
blockKey = key,
width = NativeblocksRow.WidthOptions.Match,
content = {
nativeblocksText(blockKey = "${key}Name", text = nameVar)
nativeblocksText(blockKey = "${key}Price", text = priceVar)
},
)
}
fun frame(): NativeblocksFrame = androidFrame(name = "ProductList", route = "/products") {
val name1 by remember("")
val price1 by remember("")
rootBlock {
nativeblocksColumn(blockKey = "main", content = {
productRow("row1", name1, price1)
})
}
}
```
Helpers are plain functions that return JSX:
```tsx showLineNumbers
function productRow(key: string, nameVar: Variable, priceVar: Variable) {
return (
);
}
function frame(): NativeblocksFrame {
const frame = androidFrame({ name: "ProductList", route: "/products" });
const name1 = remember("");
const price1 = remember("");
return frame.rootBlock(
{productRow("row1", name1, price1)}
);
}
```
Helpers are free functions that return a block and can be used inside any `@BlocksBuilder` closure:
```swift showLineNumbers
func productRow(key: String, nameVar: Variable, priceVar: Variable) -> some NativeblocksBlock {
NativeblocksHstack(blockKey: key) {
NativeblocksText(blockKey: "\(key)Name", text: nameVar)
NativeblocksText(blockKey: "\(key)Price", text: priceVar)
}
.width("fill")
}
private func frame() -> any NativeblocksFrame {
iosFrame(name: "ProductList", route: "/products") { f in
let name1 = remember("")
let price1 = remember("")
f.rootBlock {
NativeblocksVstack(blockKey: "main") {
productRow(key: "row1", nameVar: name1, priceVar: price1)
}
}
}
}
```
---
### [Action](https://nativeblocks.io/docs/reference/action/)
Actions (also called triggers) run when a block fires an event: a tap, a text change, a form submission, or any custom event the block defines. You attach them to a block's event slots. Multiple actions in the same slot run in declaration order.
There are two kinds:
- **Registered actions** are custom logic generated from an installed integration. See [Block & Action](/docs/sdk/block-action/) for how to build them.
- **script** is a built-in inline action for reading and writing variables and updating block properties without a separate integration.
Unlike blocks, actions have no slots and no visibility. Properties take a single value (no breakpoints), data binds a variable, and the only events are `onNext`, `onSuccess`, and `onFailure`, each chaining the next trigger.
---
## Action factory
Registered actions are factory functions. Everything is a **named argument**: `actionName`, properties, data, and the chaining events.
```kotlin showLineNumbers
analyticsAction(
actionName = "track",
route = "value", // property
payload = dataVar, // data
onNext = { // chain the next trigger
navigateAction(actionName = "nav")
},
)
```
Registered actions are factory functions that return an action you configure by chaining.
```tsx showLineNumbers
analyticsAction({ name: "track" })
.route("value") // property
.assignPayload(dataVar) // data (assign + key)
.onNext(navigateAction()) // chain the next trigger
```
Registered actions are classes. Instantiate them with `actionName` and data fields, then chain properties and events.
```swift showLineNumbers
AnalyticsAction(actionName: "track", payload: dataVar)
.route("value") // property
.onNext { // chain the next trigger
NavigateAction(actionName: "nav")
}
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `actionName` (`name` in TypeScript) | string | Unique name for this action within the frame. Auto-generated if omitted. |
---
## Properties
Action properties take a single value with no breakpoints.
```kotlin showLineNumbers
analyticsAction(actionName = "track", route = "value")
```
```tsx showLineNumbers
analyticsAction({ name: "track" }).route("value")
```
```swift showLineNumbers
AnalyticsAction(actionName: "track").route("value")
```
---
## Data
Data binds a frame variable. The action reads the variable's current value at runtime.
Data is a named argument whose name is the data key.
```kotlin showLineNumbers
navigateAction(actionName = "nav", payload = routeVar)
```
Data is a chained `assign{Key}` method.
```tsx showLineNumbers
navigateAction().assignPayload(routeVar)
```
Data is a constructor argument whose label is the data key.
```swift showLineNumbers
NavigateAction(actionName: "nav", payload: routeVar)
```
---
## Events
Actions support three chaining events. Only these three are valid:
| Event | When it runs |
|-------|-------------|
| `onNext` | After this action completes, regardless of outcome |
| `onSuccess` | Only when this action succeeds |
| `onFailure` | Only when this action fails |
Registered actions take chaining events as named-argument lambdas. The built-in `script` chains `.onNext { }`.
```kotlin showLineNumbers
requestAction(
actionName = "request",
onSuccess = { showSuccessAction(actionName = "ok") },
onFailure = { showErrorAction(actionName = "err") },
)
script { _, updateVariable, _ ->
updateVariable(stepVar, "1")
}.onNext {
script { _, updateVariable, _ ->
updateVariable(stepVar, "2")
}
}
```
Events are chained methods taking the next action(s).
```tsx showLineNumbers
requestAction()
.onSuccess(showSuccessAction())
.onFailure(showErrorAction());
script((_, updateVariable) => { updateVariable(stepVar, "1"); })
.onNext(script((_, updateVariable) => { updateVariable(stepVar, "2"); }));
```
Events are chained methods taking a closure.
```swift showLineNumbers
RequestAction(actionName: "request")
.onSuccess { ShowSuccessAction(actionName: "ok") }
.onFailure { ShowErrorAction(actionName: "err") }
script { _, updateVariable, _ in
updateVariable(stepVar, "1")
}
.onNext {
script { _, updateVariable, _ in
updateVariable(stepVar, "2")
}
}
```
---
## Attaching to blocks
Attach one or more actions to a block event. They run top to bottom.
```kotlin showLineNumbers
nativeblocksButton(
blockKey = "btn",
onClick = {
navigateAction(actionName = "nav")
script { _, updateVariable, _ ->
updateVariable(loadingVar, "true")
}
},
)
```
```tsx showLineNumbers
{ updateVariable(loadingVar, "true"); }),
]}
/>
```
```swift showLineNumbers
NativeblocksButton(blockKey: "btn")
.onClick {
NavigateAction(actionName: "nav")
script(name: "load") { _, updateVariable, _ in
updateVariable(loadingVar, "true")
}
}
```
---
## Rules
- Actions do not return values. Communicate results by writing to frame variables.
- Actions inside the same event run in declaration order.
- Use `onNext` / `onSuccess` / `onFailure` to chain actions that must run sequentially across async boundaries.
- Avoid long-running or blocking work inside `script`.
- Every action name must be unique within a frame.
---
## script
`script` is the built-in action for inline logic. The build closure receives three helper functions:
| Function | What it does |
|----------|-------------|
| `getVariable(variable)` | Produces the `getVariable("key")` read expression. The runtime returns the value as a string. |
| `updateVariable(variable, value)` | Produces an `updateVariable("key", …)` write. `value` is a template string, embed another variable to splice in its live value. |
| `updateBlockProperties(key, property, values)` | Updates a block's visual property at runtime without rebuilding the frame. |
The script body is **JavaScript** at runtime.
The build closure returns the JavaScript body string. Helper calls produce snippet strings; for computed writes, write JavaScript directly (`v.key` gives the variable's key).
```kotlin showLineNumbers
// Single templated write: return the snippet directly
script { _, updateVariable, _ ->
updateVariable(myVar, "newValue")
}
// Reference another variable's live value with {{var:key}}
script { _, updateVariable, _ ->
updateVariable(greeting, "Hello {{var:userName}}")
}
// Read then compute: splice the read expression, write raw JavaScript
script { getVariable, _, _ ->
"""
let n = Number(${getVariable(myVar)});
updateVariable("${myVar.key}", String(n + 1));
""".trimIndent()
}
```
Optional name: `script(name = "myScript") { ... }`.
The body is regular JavaScript. The CLI rewrites `getVariable(myVar)` / `updateVariable(myVar, …)` into string-keyed calls at build time.
```tsx showLineNumbers
script((getVariable, updateVariable, updateBlockProperties) => {
// Read
const value = getVariable(myVar);
// Write
updateVariable(myVar, "newValue");
// Update block property
updateBlockProperties("blockKey", "backgroundColor", { mobile: "#FF0000" });
});
```
Optional name: `script((...) => { ... }, "myScript")`.
The build closure returns the JavaScript body string. Helper calls produce snippet strings; for computed writes, write JavaScript directly (`v.key` gives the variable's key).
```swift showLineNumbers
// Single templated write: return the snippet directly
script { _, updateVariable, _ in
updateVariable(myVar, "newValue")
}
// Reference another variable's live value with {{var:key}}
script { _, updateVariable, _ in
updateVariable(greeting, "Hello {{var:userName}}")
}
// Read then compute: splice the read expression, write raw JavaScript
script { getVariable, _, _ in
#"""
let n = Number(\#(getVariable(myVar)));
updateVariable("\#(myVar.key)", String(n + 1));
"""#
}
```
Optional name: `script(name: "myScript") { ... }`.
### updateBlockProperties
The third argument is the property value per breakpoint. Pass only the breakpoints you want to change; omitted ones keep their current value. The `key` must match a block's `blockKey` in the same frame.
```kotlin showLineNumbers
// Single breakpoint
updateBlockProperties("card", "backgroundColor", BlockPropValues(mobile = "#FF0000"))
// Multiple breakpoints
updateBlockProperties("card", "backgroundColor", BlockPropValues(
mobile = "#FF0000",
tablet = "#00FF00",
desktop = "#0000FF",
))
```
```tsx showLineNumbers
// Single breakpoint
updateBlockProperties("card", "backgroundColor", { mobile: "#FF0000" });
// Multiple breakpoints
updateBlockProperties("card", "backgroundColor", {
mobile: "#FF0000",
tablet: "#00FF00",
desktop: "#0000FF",
});
```
```swift showLineNumbers
// Single breakpoint
updateBlockProperties("card", "backgroundColor", BlockPropValues(mobile: "#FF0000"))
// Multiple breakpoints
updateBlockProperties("card", "backgroundColor", BlockPropValues(
mobile: "#FF0000",
tablet: "#00FF00",
desktop: "#0000FF"
))
```
---
## Common patterns
### Boolean toggle
```kotlin showLineNumbers
script { getVariable, _, _ ->
"""updateVariable("${flagVar.key}", String(${getVariable(flagVar)} !== "true"));"""
}
```
```tsx showLineNumbers
script((getVariable, updateVariable) => {
const on = getVariable(flagVar) === "true";
updateVariable(flagVar, String(!on));
});
```
```swift showLineNumbers
script { getVariable, _, _ in
#"updateVariable("\#(flagVar.key)", String(\#(getVariable(flagVar)) !== "true"));"#
}
```
### INDEX: current item in a repeating block
`INDEX` gives the current iteration index when a `script` runs inside a repeating block. It compiles to the `{{index}}` placeholder.
```kotlin showLineNumbers
script { getVariable, _, _ ->
"let item = JSON.parse(${getVariable(listVar)})[$INDEX];"
}
```
```tsx showLineNumbers
script((getVariable) => {
const item = JSON.parse(getVariable(listVar))[INDEX];
});
```
```swift showLineNumbers
script { getVariable, _, _ in
#"let item = JSON.parse(\#(getVariable(listVar)))[\#(INDEX)];"#
}
```
---
## Helper functions
When the same action sequence repeats, extract it into a helper declared outside the frame function.
Helpers must extend `ActionsScope` so action factory calls self-register:
```kotlin showLineNumbers
fun ActionsScope.trackAndNavigate(routeVar: Variable) {
analyticsAction(actionName = "track", route = routeVar.value)
navigateAction(actionName = "nav", payload = routeVar)
}
fun frame(): NativeblocksFrame = androidFrame(name = "Home", route = "/home") {
rootBlock {
nativeblocksButton(blockKey = "btn", onClick = {
trackAndNavigate(routeVar)
})
}
}
```
Helpers are plain functions that return an action:
```tsx showLineNumbers
function trackAndNavigate(routeVar: Variable) {
return analyticsAction()
.onNext(navigateAction().assignPayload(routeVar));
}
function frame(): NativeblocksFrame {
const frame = androidFrame({ name: "Home", route: "/home" });
return frame.rootBlock(
);
}
```
Helpers are free functions that return an action and can be used inside any event closure:
```swift showLineNumbers
func trackAndNavigate(routeVar: Variable) -> some NativeblocksAction {
AnalyticsAction(actionName: "track")
.onNext {
NavigateAction(actionName: "nav", payload: routeVar)
}
}
private func frame() -> any NativeblocksFrame {
iosFrame(name: "Home", route: "/home") { f in
f.rootBlock {
NativeblocksButton(blockKey: "btn")
.onClick {
trackAndNavigate(routeVar: routeVar)
}
}
}
}
```
---
## Imports
```kotlin showLineNumbers
import com.example.app.generated.integration.android.Script.script
import com.example.app.generated.integration.android.nativeblocks.navigate.navigateAction
```
`script` and `INDEX` live in the platform `Script` package; registered actions follow the same path pattern as blocks.
```tsx showLineNumbers
import { script, INDEX } from "@nativeblocks/android/Script/Script";
import { navigateAction } from "@nativeblocks/android/nativeblocks/navigate/NativeblocksNavigate";
```
```swift showLineNumbers
// script, INDEX, and all registered actions are bundled in the platform package
import NativeblocksIOS // iosScript via script(...) + INDEX
import NativeblocksANDROID // androidScript via script(...) + INDEX
```
---
### [Overview](https://nativeblocks.io/docs/cloud/overview/)
Nativeblocks Cloud is the backend infrastructure that powers the code-push platform. It sits between your CLI and your users' devices, handling everything from frame storage to release management.
---
## What the Cloud does
| Capability | Details |
|------------|---------|
| **Frame hosting** | When you run `frame deploy` command, the compiled frame is uploaded to the cloud. The SDK fetches the latest active frame on each app open, with no App Store or Play Store review needed. |
| **Release management** | The cloud tracks versioned frame tags, supports targeted rollouts (by user, app version, or percentage), and lets you roll back to any previous tag instantly. |
| **Studio backend** | The web studio for monitoring tag distribution, daily active users, and triggering rollbacks is backed by the cloud. |
| **Component registry** | When you annotate components with `@NativeBlock` / `@NativeAction` and sync them, they are registered with the Nativeblocks servers so the CLI can generate typed DSL wrappers. |
---
## How it works
1. You write a frame in Kotlin, TypeScript, or Swift and run `frame deploy` command.
2. The compiled frame is uploaded to the cloud and linked to the tag.
3. The Studio lets you promote the tag to production (or roll it out gradually).
4. On the next app open the SDK fetches the active frame and renders it without any new release.
## Deployment options
| Option | Description |
|--------|-------------|
| **Nativeblocks Cloud** | Fully managed. Sign up at [nativeblocks.io](https://nativeblocks.io) and start deploying immediately. |
| **Core API Self-Hosted** | Run `nativeblocks-core-api` on your own infrastructure with Docker. Requires a license key. See [Core API Self-Hosting](/docs/cloud/core-api-self-hosting/). |
| **Studio Self-Hosted** | Run `nativeblocks-studio` on your own infrastructure with Docker. Connects to a self-hosted or cloud Core API. See [Studio Self-Hosting](/docs/cloud/studio-self-hosting/). |
---
## Reference architecture
This is how a Nativeblocks stack plugs into a typical product. Your platform never renders Nativeblocks UI; it only hands the client SDK its `apiKey` and `apiUrl`. Whether you run Core API and Studio yourself or use Nativeblocks Cloud, the call paths are the same.
### Runtime: serving UI to the app
On app start, your app's config (or init) API returns `apiKey` and `apiUrl` (the SDK gateway URL) to the client SDK. The SDK then calls Core API's `/gateway/init` directly with those credentials and renders the returned frames natively.
### The stack & operations
Nginx terminates TLS in front of Core API (`:8080`) and Studio (`:80`). Core API uses PostgreSQL (migrations auto-run at startup), Valkey/Redis (cache + event queue), and an S3-compatible bucket (assets). It validates its license against `admin.api.nativeblocks.io` at startup and periodically, pushes hot-reload events to the realtime service, and exposes `/metrics` for Prometheus to scrape. Logs go to stdout and to whatever log manager you prefer (Sentry, Filebeat, or anything else).
---
---
### [Core API Self-Hosting](https://nativeblocks.io/docs/cloud/core-api-self-hosting/)
This guide covers deploying `nativeblocks-core-api` on your own infrastructure.
---
## Prerequisites
| Requirement | Version | Notes |
|-----------------------|---------|--------------------------------------------------------|
| Docker | 24+ | Recommended deployment method |
| PostgreSQL | 15+ | Primary database |
| Valkey / Redis | 7+ | Cache and event queue |
| S3-compatible storage | — | AWS S3, DigitalOcean Spaces, MinIO, etc. |
| License key | — | Obtain from [nativeblocks.io](https://nativeblocks.io) |
Database migrations run automatically at startup. No manual migration step is required.
---
## Environment Variables
| Variable | Required | Description |
|---------------------------|----------|----------------------------------------------------------------------------------------------|
| `PORT` | No | HTTP port (default: `8080`) |
| `POSTGRES_CONNECTION_URL` | Yes | PostgreSQL DSN, e.g. `postgresql://user:pass@host:5432/dbname?sslmode=require` |
| `VALKEY_CONNECTION_URL` | Yes | Valkey/Redis URL, e.g. `redis://host:6379` or `rediss://host:6380` |
| `S3_ACCESS_KEY_ID` | Yes | S3-compatible access key |
| `S3_SECRET_ACCESS_KEY` | Yes | S3-compatible secret key |
| `S3_ENDPOINT` | Yes | S3 endpoint URL, e.g. `https://s3.amazonaws.com` or `https://fra1.digitaloceanspaces.com` |
| `LICENSE_KEY` | Yes | Nativeblocks license key |
| `ADMIN_ENDPOINT` | Yes | Nativeblocks admin API — use `https://admin.api.nativeblocks.io` unless instructed otherwise |
| `CORE_ENDPOINT` | Yes | Publicly reachable URL of this service, e.g. `https://api.yourcompany.com` |
| `GATEWAY` | Yes | SDK delivery format: `GRAPHQL` |
| `LOG_LEVEL` | No | Log verbosity for `stdout`: `verbose` (default), `warning`, `error` |
| `LOG_PROVIDERS` | No | Comma-separated providers: `stdout` (default), `sentry` |
| `SENTRY_DSN` | No | Sentry DSN — required when `sentry` is in `LOG_PROVIDERS`. Sentry only receives error-level logs regardless of `LOG_LEVEL` |
---
## Logger Configuration
| Variable | Values | Default | Notes |
|-----------------|-------------------------------------|-----------|----------------------------------------------------|
| `LOG_LEVEL` | `verbose` · `warning` · `error` | `verbose` | Applies to `stdout` only |
| `LOG_PROVIDERS` | comma-separated: `stdout`, `sentry` | `stdout` | |
| `SENTRY_DSN` | Sentry DSN URL | — | Sentry always receives error-level logs only; `LOG_LEVEL` does not affect it |
Examples:
```bash
# stdout only (development)
LOG_LEVEL=verbose
LOG_PROVIDERS=stdout
# stdout (verbose) + Sentry (production)
# Note: Sentry only captures errors regardless of LOG_LEVEL
LOG_LEVEL=verbose
LOG_PROVIDERS=stdout,sentry
SENTRY_DSN=https://...@sentry.io/123
```
---
## Docker Deployment
### Pull and run
```bash
docker pull nativeblocks/core-api:latest
docker run -d \
--name nativeblocks-core-api \
--restart unless-stopped \
-p 8080:8080 \
-e PORT=8080 \
-e POSTGRES_CONNECTION_URL="postgresql://user:pass@your-db-host:5432/nativeblocks?sslmode=require" \
-e VALKEY_CONNECTION_URL="redis://your-valkey-host:6379" \
-e S3_ACCESS_KEY_ID="" \
-e S3_SECRET_ACCESS_KEY="" \
-e S3_ENDPOINT="https://s3.amazonaws.com" \
-e GATEWAY=GRAPHQL \
-e ADMIN_ENDPOINT=https://admin.api.nativeblocks.io \
-e CORE_ENDPOINT=https://api.yourcompany.com \
-e LICENSE_KEY="" \
-e LOG_LEVEL=error \
-e LOG_PROVIDERS=stdout,sentry \
-e SENTRY_DSN="" \
nativeblocks/core-api:latest
```
---
## Docker Compose Deployment
Create a `docker-compose.yml` for a full self-contained stack:
```yaml
version: "3.9"
volumes:
postgres_data:
valkey_data:
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: nativeblocks
POSTGRES_USER: nativeblocks
POSTGRES_PASSWORD:
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U nativeblocks" ]
interval: 10s
timeout: 5s
retries: 5
valkey:
image: valkey/valkey:8
restart: unless-stopped
volumes:
- valkey_data:/data
healthcheck:
test: [ "CMD", "valkey-cli", "ping" ]
interval: 10s
timeout: 5s
retries: 5
core-api:
image: nativeblocks/core-api:latest
restart: unless-stopped
ports:
- "8080:8080"
depends_on:
postgres:
condition: service_healthy
valkey:
condition: service_healthy
environment:
PORT: "8080"
POSTGRES_CONNECTION_URL: "postgresql://nativeblocks:@postgres:5432/nativeblocks?sslmode=disable"
VALKEY_CONNECTION_URL: "redis://valkey:6379"
S3_ACCESS_KEY_ID: ""
S3_SECRET_ACCESS_KEY: ""
S3_ENDPOINT: "https://s3.amazonaws.com"
GATEWAY: "GRAPHQL"
ADMIN_ENDPOINT: "https://admin.api.nativeblocks.io"
CORE_ENDPOINT: "https://api.yourcompany.com"
LICENSE_KEY: ""
LOG_LEVEL: "error"
LOG_PROVIDERS: "stdout,sentry"
SENTRY_DSN: ""
```
```bash
docker compose up -d
```
---
## Endpoints
| Path | Purpose |
|-----------------------|------------------------------|
| `/graphql` | Main GraphQL API |
| `/graphql/playground` | Interactive GraphQL explorer |
| `/gateway/init` | SDK gateway initialization |
| `/metrics` | Prometheus metrics |
---
## Reverse Proxy (Nginx)
```nginx
server {
listen 443 ssl;
server_name api.yourcompany.com;
ssl_certificate /etc/ssl/certs/yourcompany.crt;
ssl_certificate_key /etc/ssl/private/yourcompany.key;
client_max_body_size 20M;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}
server {
listen 80;
server_name api.yourcompany.com;
return 301 https://$host$request_uri;
}
```
---
## Database Migrations
Migrations run automatically when the server starts. No manual intervention is needed. If you need to verify migration state, the server logs the current and target version at startup:
```
Current DB Version: 12, Latest Version: 12
No migrations needed.
```
---
## License Validation
The service connects to `ADMIN_ENDPOINT` to validate the license on startup and periodically while running. If validation fails, the process exits. Ensure:
- `ADMIN_ENDPOINT` is reachable from the container
- `LICENSE_KEY` is valid and not expired
- Outbound HTTPS traffic is allowed from the host
You can use `https://admin.api.nativeblocks.io` for the env variable.
---
## Monitoring
Prometheus metrics are exposed at `/metrics`. Scrape config example:
```yaml
scrape_configs:
- job_name: nativeblocks-core-api
static_configs:
- targets: [ "api.yourcompany.com:8080" ]
metrics_path: /metrics
```
---
## Upgrading
```bash
# Pull the new image
docker pull nativeblocks/core-api:
# Restart the container (migrations run automatically)
docker compose up -d --no-deps core-api
```
Migrations are applied forward-only. Always back up PostgreSQL before upgrading to a new major version.
---
## Minimum Resource Recommendations
| Component | CPU | Memory |
|-----------|--------|--------|
| core-api | 2 vCPU | 2 GB |
---
### [Studio Self-Hosting](https://nativeblocks.io/docs/cloud/studio-self-hosting/)
This guide covers deploying `nativeblocks-studio` on your own infrastructure.
**Note:** The Studio is a frontend application that connects to a running `nativeblocks-core-api` instance. Make sure your core API is deployed and reachable before setting up the Studio.
---
## Prerequisites
| Requirement | Notes |
|-----------------------|--------------------------------------------------------|
| Docker | Recommended deployment method |
| nativeblocks-core-api | Running and publicly reachable |
---
## Environment Variables
Environment variables are injected at container startup and made available to the app at runtime.
| Variable | Required | Description |
|-----------------------------------|----------|-----------------------------------------------------------------------------|
| `VITE_WEBSITE_URL` | Yes | Dashboard URL, e.g. `https://nativeblocks.io/dashboard` |
| `VITE_API_ENDPOINT_GRAPHQL_URL` | Yes | Core API GraphQL endpoint, e.g. `https://api.yourcompany.com/graphql` |
| `VITE_API_REALTIME_ENDPOINT_URL` | Yes | Realtime WebSocket endpoint, e.g. `wss://api.yourcompany.com/hotReload` |
| `VITE_SENTRY_DSN` | No | Sentry DSN for error reporting |
---
## Docker Deployment
### Pull and run
```bash
docker pull nativeblocks/nativeblocks-studio:selfhost
docker run -d \
--name nativeblocks-studio \
--restart unless-stopped \
-p 80:80 \
-e VITE_WEBSITE_URL="https://nativeblocks.io/dashboard" \
-e VITE_API_ENDPOINT_GRAPHQL_URL="https://api.yourcompany.com/graphql" \
-e VITE_API_REALTIME_ENDPOINT_URL="wss://api.yourcompany.com/hotReload" \
-e VITE_SENTRY_DSN="" \
nativeblocks/nativeblocks-studio:selfhost
```
---
## Docker Compose Deployment
```yaml
version: "3.9"
services:
studio:
image: nativeblocks/nativeblocks-studio:selfhost
restart: unless-stopped
ports:
- "80:80"
environment:
VITE_WEBSITE_URL: "https://nativeblocks.io/dashboard"
VITE_API_ENDPOINT_GRAPHQL_URL: "https://api.yourcompany.com/graphql"
VITE_API_REALTIME_ENDPOINT_URL: "wss://api.yourcompany.com/hotReload"
VITE_SENTRY_DSN: ""
```
```bash
docker compose up -d
```
---
## Reverse Proxy (Nginx)
```nginx
server {
listen 443 ssl;
server_name studio.yourcompany.com;
ssl_certificate /etc/ssl/certs/yourcompany.crt;
ssl_certificate_key /etc/ssl/private/yourcompany.key;
location / {
proxy_pass http://127.0.0.1:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name studio.yourcompany.com;
return 301 https://$host$request_uri;
}
```
---
## Upgrading
```bash
docker pull nativeblocks/nativeblocks-studio:selfhost
docker compose up -d --no-deps studio
```
---
## Minimum Resource Recommendations
| Component | CPU | Memory |
|-----------|----------|--------|
| studio | 0.5 vCPU | 512 MB |