# 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 |