FuuzHelp Center

Build Apps · Start here

Technical Guide: Build on Fuuz Like You Built It

Last updated October 26, 2026, 12:00 AM UTC · fuuz.com/help/fuuz-technical-guide

Audience
Solution engineers, developers and partner builders
Applies to Versions
2026.10+

Overview

Every screen, data flow, integration and data model you create in Fuuz runs on the same stack: MongoDB, GraphQL APIs, the Data Flow Engine, Fuuz JSONata bindings and a deploy-anywhere infrastructure, with real-time updates over WebSockets. Knowing how each layer works, and how the layers talk to each other, is how you write better screens, faster flows and cleaner integrations, and how you find problems in minutes instead of hours.

This guide is written for engineers coming from SQL-backed, relational or legacy ERP and MES environments.

The stack

Layer What it is
Client layer Screen Designer screens, mobile, external apps and IoT devices
Data Flow Engine The low-code and pro-code workflow engine: web, backend and gateway flows
Transformation layer Fuuz JSONata bindings and JavaScript: $state, $components, $metadata
GraphQL APIs Multiple typed, schema-validated APIs
MongoDB A JSON-native, multi-tenant document store with indexed collections
Infrastructure Deploy-anywhere: auto-scaling, CDN, identity and access management, high availability and managed operations

How the layers talk to each other: MongoDB reads and writes BSON documents; GraphQL serves typed queries, mutations and predicates; flows and screens reshape results with JSONata or JavaScript; and real-time events arrive over WebSockets.

Tip: The stack gives you a clear way to isolate problems. Data wrong? Check the document shape with a GraphQL query. Query returning unexpected results? Check the predicates and filters. Transform producing bad output? Check the JSONata or JavaScript expression in the transform console. Screen not rendering? Inspect the element binding.

Real time: WebSockets, not GraphQL subscriptions

Live screen refresh, push events and shop-floor alerts are delivered by a dedicated WebSocket subscription service, separate from the GraphQL APIs. The result is the same, live data without polling, but it is a distinct service, not a GraphQL subscription.

MongoDB: think in documents, not tables

This is the biggest shift for engineers from SQL-backed environments. MongoDB stores JSON documents, not rows, and how you design documents affects query performance, screen bindings and flow logic.

Concept SQL world MongoDB and Fuuz
Data unit Row in a table JSON document in a collection
Relationships Foreign keys and joins Nested sub-documents or ID references
Schema changes ALTER TABLE, a migration and risk Add the field in the schema designer; it is live with near-zero downtime
Optional data A nullable column on every row The field is simply absent when it does not apply
Variable shapes EAV tables or wide nullable rows Each document carries its own fields
Filtering and aggregation SQL WHERE and GROUP BY GraphQL predicates and _aggregate queries at the API layer
Record ID Primary key (varies) Always id in Fuuz GraphQL, with no underscore

In a relational MES or ERP, reconstructing one work order can take four tables and three joins, and adding a field means ALTER TABLE, a migration and a deployment window:

SELECT wo.id, wo.number, wo.status, p.name, p.revision, op.seq, op.description
FROM work_orders wo
JOIN parts p ON p.id = wo.part_id
JOIN wo_operations op ON op.wo_id = wo.id
WHERE wo.id = 'WO-1042';

In Fuuz, the whole object comes back in one read, and a new field such as customer_po is added in the schema designer:

{
  "id": "WO-1042",
  "number": "WO-1042",
  "status": "In Progress",
  "part": { "id": "part-ref-id", "name": "Bracket Assy", "revision": "C" },
  "operations": [],
  "customer_po": "PO-8812"
}

Because adding a field takes near-zero downtime, you can extend your applications without losing valuable production time.

Embedding: the right mental model, arriving in early 2027

The embed-versus-reference pattern is the right way to think about a document database. Today, Fuuz supports JSON schema and JSON objects within data models, and enumerations are handled by joining models, which keeps them clean and queryable. Full embedded document support is on the roadmap for early 2027. Apply the principles in your data models now, and they will be well placed as that support arrives.

Important: The most common mistake from SQL backgrounds is over-normalizing: creating a separate model for every sub-entity. Before you split, ask whether the data will ever be queried on its own, or whether it always travels with its parent. Fewer models often mean simpler queries, cleaner flows and easier screen bindings.

GraphQL APIs: where filtering and fetching live

Fuuz exposes multiple GraphQL APIs. This is where you declare the data you want, apply filters and predicates, and run aggregations. When you create or extend a data model, its GraphQL API is generated and updated automatically.

Conventions to know before you build

Good habits:

The GraphQL editor autocompletes and validates against the schema as you type, so field-name errors and type mismatches are caught before runtime.

Query with predicates

query GetOpenWOs($where: WorkOrderWhereInput, $orderBy: [WorkOrderOrderByInput!], $first: Int, $after: String) {
  workOrder(where: $where, orderBy: $orderBy, first: $first, after: $after) {
    pageInfo { startCursor endCursor hasNextPage hasPreviousPage }
    total
    edges {
      node {
        id
        number
        status
        scheduledStart
        part { name revision }
      }
    }
  }
}
{
  "where": { "workCenterId": { "_eq": "wc-123" }, "status": { "_eq": "Open" } },
  "orderBy": [{ "field": "scheduledStart", "direction": "asc" }]
}

Mutation

mutation UpdateWOStatus($id: ID!, $status: String!, $completedAt: DateTime) {
  updateWorkOrder(id: $id, data: { status: $status, completedAt: $completedAt }) {
    id
    status
    completedAt
  }
}

Aggregate query

The group field goes on the node, with _aggregate beside it. Each edge.node in the result contains the group field and its count.

query WOStatusCounts($where: WorkOrderWhereInput, $orderBy: [WorkOrderOrderByInput!]) {
  workOrder(where: $where, orderBy: $orderBy) {
    total
    edges {
      node {
        workOrderStatusId
        _aggregate { id { count } }
      }
    }
  }
}

See also Aggregate Query API.

Anti-pattern: filtering in the wrong layer

Do not fetch every record and filter in a JSONata transform such as $[status = "Open"]. Filter in the query instead:

query {
  workOrders(where: { status: { _eq: "Open" } }) {
    id
    status
    number
  }
}

API Explorer and Query Builder

The fastest way to learn Fuuz GraphQL is to let the built-in tools write it for you. Go to System > Business Intelligence > API Explorer in any tenant. The Query Builder is under Tools at the top of the editor, and everything runs against your tenant's schema in real time.

A learning path

  1. Open API Explorer > Tools > Query Builder.
  2. Select your model and check the fields you need.
  3. Click the pencil on a field to read its description and arguments.
  4. Click $ to see the variables and their types.
  5. Click ** GraphQL Code** to view the generated query.
  6. Copy the query into your screen binding or flow node, and test it in the Explorer first.

Do this a dozen times across different models and Fuuz GraphQL will feel natural.

Data Flow Engine

The Data Flow Engine sits above the GraphQL layer. It is your application's business logic layer: built visually, with the option to drop into JavaScript for complex operations. Flows call GraphQL APIs to read and write data, then use the transformation layer to process the results. Keep that order in mind when you trace what triggered what.

Flow type What it does
Web flows Triggered by user interactions on screens. They also act as remote function calls (RFC) that any external system can call. Web flows with screen context can use $components and screen elements directly.
Backend flows Server-side logic triggered by events, schedules or API calls: integrations, data processing, ERP write-backs and anything that should not run on the client.
Gateway flows Expose Fuuz logic to external systems as callable endpoints, so they can push data in or trigger operations.

Fuuz JSONata bindings

Standard JSONata is the foundation; Fuuz extends it with more than 200 platform bindings for screen state, user identity, flow context, joins and more. For the standard language, see the JSONata tutorial and the custom Fuuz-only JSONata library.

Two execution contexts

$components: work with screen elements

Expression What it does
$components.Form1.fn.save() Save the form
$components.Form1.fn.setValue("field", val) Set one field value
$components.Form1.formState.dirty True when there are unsaved changes
$components.Form1.formState.valid True when all validations pass
$components.Form1.data.fieldName Read a field value
$components.Table1.fn.search() Refresh the table
$components.Table1.selectedRows All selected rows
$components.Table1.selectedRows[0].id The first selected row's ID
$components.Screen.fn.setContextValue("k", v) Set one screen context key
$components.Screen.fn.mergeContext({...}) Shallow-merge values into the screen context
$components.Screen.context.myKey Read a screen context value

$metadata: URL, user and tenant

Expression What it does
$metadata.urlParameters.id URL path parameter, such as /record/:id
$metadata.querystring.tab Query string parameter, such as ?tab=details
$metadata.user.id Signed-in user's ID
$metadata.user.email Signed-in user's email
$metadata.user.roles The user's roles
$metadata.tenant.id Current tenant ID
$metadata.tenant.name Current tenant name
$metadata.settings.timezone The user's time zone
$metadata.settings.dateFormat The user's date format

$state: data flow context

Expression What it does
$ The current node payload, shorthand for $state.payload
$state.payload The full input payload of the current node
$state.context Shared flow context, set with Set Context or Merge Context nodes
$state.context.myKey Read one context value
$state.claims.userId The triggering user's ID
$state.claims.tenantId Tenant ID from the auth claims
$state.claims.roles The user's roles from the auth claims
$state.metadata.flowId The current flow's ID
$state.lastError The error from the last caught error node

data, $card and $$: element-level bindings

Expression What it does
data.fieldName Read a field value inside a form element
data.active ? "Yes" : "No" A condition on a boolean form field
$card.data.id The card record's ID inside a cards element (not data.id)
$card.data.status A field value on the card record
$$.newValue The new value in an onChange handler
$$.oldValue The previous value in an onChange handler
$isNotNilOrEmpty($$.newValue) and $not($$.newValue = $$.oldValue) Fire only when the value is not empty and actually changed

Join functions and utility bindings

/* $innerJoin: match records from both sides */
$innerJoin(workOrders, parts, "partId", "id")

/* $leftOuterJoin: keep every left record, match the right */
$leftOuterJoin(workOrders, scheduledShifts, "shiftId", "id")

/* $query: run a GraphQL query inline */
$query({ "api": "system", "statement": "query { ... }", "variables": { "id": "123" } })

/* $numeral: number formatting */
$numeral(12345).format("0,0")        /* "12,345" */
$numeral(0.75).format("0%")          /* "75%" */
$numeral(1234.5).format("$0,0.00")   /* "$1,234.50" */

$now               /* ISO 8601 timestamp */
$appConfig.someKey /* tenant app configuration */

Syntax gotchas

Gotcha Correct usage
No ! operator Use $not(expr)
String concatenation is & "a" & " " & "b", not +
Equality is = x = "val", not == or ===
Membership x in [1,2,3], not .includes()
No ?? operator $exists(val) ? val : "default"
Cards versus forms Inside cards, use $card.data.x, not data.x
Context is $ $ is the whole current input; confirm its shape in the console before writing transforms

Coming from SQL? Reframe these habits

These are the closest useful parallels, not a one-to-one mapping.

SQL and relational Fuuz
Normalize everything and rebuild the object with joins at query time. Design for your access pattern. Data that always travels together belongs together.
Schema changes are risky: ALTER TABLE, migrations, deployment windows and table locks. Add a field in the schema designer with near-zero downtime. Field changes are low-risk, fast tasks.
Full embedded documents are expected in document databases. Fuuz supports JSON schema and JSON objects in data models today, with full embedded document support on the roadmap for early 2027. Enumerations are joins between models.
You need a database diagram and documentation just to start a query. Go straight to the Query Builder, where every relationship is a click away, and use the Application Graph to see how models, screens and flows connect.
Filter and aggregate with WHERE and GROUP BY. Filter with where variables, aggregate with edges.node.fieldName and _aggregate, and sort with orderBy. JSONata reshapes; it does not filter.
Combine tables with INNER JOIN and LEFT JOIN in the database. Combine results from several GraphQL queries in a flow transform with $innerJoin and $leftOuterJoin.
Backups, upgrades, scaling and disaster recovery are your team's job. In Fuuz-hosted environments, infrastructure is managed and your focus is application delivery. Application performance on large datasets is still a design responsibility.

Application Graph

The Application Designer includes the Application Graph: a live, visual knowledge graph of your application that shows every relationship between data models, screens and data flows in one interactive view. It reflects your actual application, not a static diagram.

Build patterns

  1. Design the query before the model. Start with what the screen needs to show, and shape the document to make that query simple.
  2. Filter in GraphQL, reshape in JSONata. Filter with where, count and sum with _aggregate, sort with orderBy, and use JSONata only to reshape the filtered response.
  3. Always return id in mutations. It confirms the write and feeds chained operations.
  4. Check the console before assuming the problem is upstream. Every transform shows its input and output.
  5. Request only what the screen uses. Over-fetching adds payload and makes bindings harder to read.
  6. Know your JSONata context. Screens use $components, $metadata, data and $card; flows use $state. Mixing them up fails silently.
  7. Map ERP writes to mutations early. When you scope an ERP integration, such as NetSuite, Plex or Business Central, identify the target mutations before writing flow logic.
  8. Treat field additions as low-risk. Adding a field is fast and has near-zero downtime. The real effort is in flow logic, behavior changes and performance tuning on large collections.

The build loop: confirm the document shape in the schema designer, write the GraphQL query with predicates, reshape the result with JSONata, bind it to the screen element or flow output, then inspect each layer in the console and logs before calling it done.

Troubleshooting: debug by layer

Start at the data layer and work up, every time.

Symptom Suspect layer Where to look
Screen shows no data GraphQL query or predicate Run the query on its own. Does the predicate return records? Is the field name right?
Wrong data on screen JSONata binding or wrong field Check the raw GraphQL response first. If it is right, check the JSONata expression in the transform console.
Screen loads slowly Dynamic expressions or missing indexes Usually dynamic bindings, such as visibility expressions, running too often, or complex filters on large collections without an index.
Flow not triggering Trigger configuration Confirm the trigger event matches the action, and check the flow execution logs.
Flow fails mid-run A specific node Open the flow execution history to find the failing node, and inspect the input it received in the console.
Mutation fails Field name or type mismatch Read the error; GraphQL errors are explicit. Remember id, not _id.
Integration writes missing Mutation not running or wrong tenant Confirm the mutation ran in the execution logs, check the tenant context and check the id returned.
JSONata returns null Wrong context or field path Are you in a screen or a flow? $state works only in flows, $components and $metadata only in screens, and cards use $card.data.x.

The debug order:

  1. Does the data exist? Run a GraphQL query to confirm.
  2. Does the query return it correctly? Run the exact binding query, with its predicates, on its own.
  3. Does the JSONata transform it correctly? Inspect the input and output in the transform console.
  4. Does the screen element bind correctly? Check the element's property mapping.

Skipping steps leads to false diagnoses.

What managed infrastructure means for your scope

When Fuuz hosts your environment, it changes what belongs in your delivery scope.

At a glance: adding a field to a model takes near-zero downtime, and one GraphQL call returns a fully hydrated object graph.

Related articles