Portable Navigation

Portable miniapp navigation lets a miniapp describe its own route graph, while the host app chooses whether to render it with the built-in portable renderer or React Navigation.

What Ships In The Manifest

A manifest-backed miniapp now has two parts: a components[] array containing the compiled screen artifacts, and a navigation object describing how those screens behave as an app.

JSON
{
  "version": 1,
  "rootId": "root-tabs",
  "nodes": [
    {
      "kind": "tabs",
      "id": "root-tabs",
      "name": "Root",
      "route": { "path": "/" },
      "variant": "bottomTabs",
      "initialRouteId": "home-stack",
      "children": [
        {
          "kind": "stack",
          "id": "home-stack",
          "name": "Home",
          "route": { "path": "/home", "title": "Home" },
          "initialRouteId": "home-page",
          "children": [
            {
              "kind": "page",
              "id": "home-page",
              "name": "HomePage",
              "componentKey": "demo/HomePage",
              "route": { "path": "/home" }
            }
          ]
        }
      ]
    }
  ]
}

Authoring Rules

  • Every route id must be unique.
  • Every page path must be unique.
  • rootId must point to a stack or tabs navigator, not a page.
  • Every navigator initialRouteId must reference one of its direct children.
  • Every page componentKey must exist in components[].
  • modal and fullScreenModal pages must live under a stack.

Target Navigation Engine

Each project (and each codegen request) declares a targetNavigationEngine so the server can validate the manifest against the rules of the runtime that will render it.

  • manifest - Default. Portable router; no react-navigation-specific constraints.
  • react-navigation - Strict: every visible child of a bottomTabs navigator must declare a non-empty tabItem.icon. For stack children, the requirement applies to the stack's initial page. To omit an icon, hide the tab with tabItem.hidden: true. When present, tabItem.iconLibrary tells the host which icon pack to use.
  • auto - Let the host decide at runtime; non-strict validation.

Resolution order (first match wins): targetNavigationEngine on the codegen request → .dpage/project.json from dpage link→ the project's server default (Project Settings → Navigation Engine) → manifest.

JSON
{
  "kind": "tabs",
  "id": "root-tabs",
  "variant": "bottomTabs",
  "initialRouteId": "home-page",
  "children": [
    {
      "kind": "page",
      "id": "home-page",
      "name": "Home",
      "componentKey": "app/HomeScreen",
      "route": { "path": "/home" },
      "tabItem": {
        "icon": "home",
        "iconLibrary": "feather",
        "label": "Home"
      }
    },
    {
      "kind": "page",
      "id": "profile-page",
      "name": "Profile",
      "componentKey": "app/ProfileScreen",
      "route": { "path": "/profile" },
      "tabItem": {
        "icon": "account-circle-outline",
        "iconLibrary": "material-community",
        "label": "Profile"
      }
    }
  ]
}

Why icons become required

React Navigation's bottom tab bar renders an icon slot for every visible tab. Missing icons produce an empty, unusable tab area, so the server rejects such manifests when a project explicitly targets react-navigation. Projects targeting manifest (the default) or auto keep icons optional. tabItem.iconLibrary is optional for legacy manifests but recommended for new ones.

Page Params Contracts

Pages can declare structured param contracts with paramsSchema and optional initialParams. Navigation calls validate params before route state changes.

JSON
{
  "kind": "page",
  "id": "book-details-page",
  "name": "BookDetailsPage",
  "componentKey": "books/Details",
  "route": { "path": "/book-details" },
  "paramsSchema": {
    "type": "object",
    "properties": {
      "bookId": { "type": "string" },
      "page": { "type": "number" }
    },
    "required": ["bookId"],
    "additionalProperties": false
  },
  "initialParams": { "page": 1 }
}
TSX
const router = useMiniappRouter();
router.push('/book-details', { bookId: 'book-123', page: 2 });
router.back();

const route = useMiniappRoute<{ bookId: string; page?: number }>();
console.log(route.params.bookId);

Validation behavior

Invalid paramsSchema, invalid initialParams, or invalid runtime params are rejected during normalization/navigation instead of entering the stack with bad state.

Generation checklist

Portable miniapp pages use router.back(), not router.goBack() or navigation.goBack(). Every route.params.someKey access must also have someKey declared in that page node's paramsSchema.properties, for example membersJson when reading route.params.membersJson.

Host Integration Matrix

EngineBest ForNotes
Portable rendererZero-host-setup embeds and demo flowsSupports manifest theme options plus renderPortableHeader and renderPortableTabBar.
React Navigation rendererNative stacks/tabs and deeper host integrationHonors portable chrome plus nativeOptions.reactNavigation. Stack headers are hidden by default unless the manifest explicitly opts in or provides header styling.
Expo Router adapterCoordinating miniapp state with host router URLsKeeps dpage as the route owner while letting the host router mirror path changes.

Runtime Example

TSX
import { MiniappComponent } from '@dpage-ai/react-native-dpage';

<MiniappComponent
  miniappId="support-workflow"
  initialTarget="/home/details"
  navigationEngine="auto"
/>;

Offline And Cache Behavior

Multi-page miniapps keep their normalized manifest and initial path in miniapp cache metadata. That means an already-cached miniapp can still boot into the correct route graph when the network is unavailable.

Legacy miniapps still work

If a miniapp payload has no navigation field, the runtime automatically synthesizes a one-screen stack manifest from the old root: true contract.

AI-Generated Miniapps

AI-generated miniapps can now publish multiple component artifacts plus one portable manifest. The publish pipeline writes every screen to KV, rewrites the manifest to the published component keys, and stores the final bundle in the miniapp index.