Building a custom IoT app rarely means building every part of the IoT backend yourself.
The hard, scalable part is usually the connected product infrastructure: device data, users, permissions, teams, resources, messages and APIs that can keep working as your fleet grows. Once that layer exists, the more interesting work is often the business logic on top: customer workflows, internal tools, reporting endpoints, onboarding flows and integrations with the systems your company already uses.
The core pattern is simple: use an IoT platform for the infrastructure layer, then build your custom business logic on top.
Firebase Cloud Functions are a practical way to add that custom backend logic around Connhex without running a dedicated backend service.
In a Connhex project, you may need server-side code that sits between your application, your business systems and the Connhex APIs. That code can validate an operation, combine multiple Connhex API calls into a single workflow or expose an integration endpoint for another system.
In this post, we’ll look at how to use Firebase HTTPS functions as that integration layer for a custom IoT application. We’ll use a simple example from the Connhex Firebase Examples repository: a Firebase function that receives a Connhex bearer token, checks whether the caller has the required Connhex permissions, and then adds a user to a team.
The flow is simple:
- expose a Firebase
onRequestHTTPS function - extract the Connhex bearer token from the request
- use Connhex IAM to introspect the permissions required by the operation
- call Connhex APIs from the Firebase function only if the caller is allowed
Why build custom IoT app backends this way
Connhex already provides the APIs for managing connected products, users, teams, rules, resources, messages and other IoT platform concerns. Firebase Cloud Functions are useful when you want to wrap those APIs with project-specific backend behavior.
This is a good fit when you want the IoT infrastructure to remain the system of record, while your own application owns the business-specific parts. For example, you might use Connhex for device data storage, permissions and connected product APIs, then use Firebase functions for the workflows that make the product feel like yours.
There are two common patterns, which we’ll discuss below.
User-delegated functions
The first pattern is a user-delegated function. The caller sends a Connhex bearer token to the Firebase function. The function checks what the user is allowed to do, then performs the operation on behalf of that user.
This is useful when the operation is triggered by a user action and should respect that user’s Connhex permissions. For example:
- expose one custom IoT app endpoint that internally calls multiple Connhex APIs
- run some custom validation before allowing a user to perform an operation
- keep frontend code simpler by moving Connhex workflow logic to the backend
Admin functions
The second pattern is a service-side or admin function. In this case, the Firebase function owns the Connhex credentials. Those credentials are stored in Firebase configuration or secrets, and the function uses them to perform trusted backend operations.
This works well for system workflows that should not depend on the permissions of the user making the request. For example:
- provision Connhex resources when a customer, tenant, site or device is created in another system
- handle a webhook from an ERP, CRM, external service or manufacturing system and translate it into Connhex operations
- run scheduled synchronization jobs between Connhex and Firebase, a database, a data warehouse or a custom reporting tool
- execute admin-only workflows without exposing admin credentials to frontend code
The example in this post uses the first pattern. It is a good starting point because it shows the core idea: the Firebase function does not blindly trust the request. It asks Connhex IAM whether the caller can perform the specific operation.
Callable vs onRequest functions
Firebase supports different ways to expose Cloud Functions. For HTTP-style integrations, the two most common options are Callable functions and onRequest functions.
Callable functions are convenient when your client application is already built around Firebase. The Firebase client SDK handles the call format, and the function integrates naturally with Firebase Auth and Firebase-specific request metadata.
Connhex integrations usually need a more explicit HTTP boundary. A Connhex request carries authentication through:
- an
Authorization: Bearer <connhex-user-token>header - a
chx_auth_sessioncookie
For this reason, onRequest is usually the better fit. It gives the function direct access to the incoming HTTP request, including headers, cookies, method and body. That makes the integration easier to reason about and easier to call from different clients.
For cookie-based UI integrations, the UI should be hosted on the same domain as the Connhex instance so the Connhex session cookie can be sent correctly.
What we’ll build
We’ll build a Firebase HTTPS function named onboard_user_to_team.
The function receives a JSON body with the target team and the Connhex membership (user identity) to add:
{
"teamId": "team-id",
"membershipId": "membership-id"
}The request must include a Connhex bearer token:
Authorization: Bearer <connhex-user-token>The function then:
- extracts the bearer token
- checks whether the token has the Connhex permissions required for the operation
- reads the team and its current users
- adds the membership only if it is not already in the team
- returns a simple success response
Declaring the Firebase function
The Firebase entrypoint is a normal onRequest HTTPS function:
import logging
from connhex.authz import UnauthorizedError
from connhex.config import CONNHEX_DOMAIN
from firebase_functions import https_fn
from flask import Request, Response
@https_fn.on_request()
def onboard_user_to_team(req: Request) -> Response:
try:
body = req.get_json(silent=True) or {}
if not isinstance(body, dict):
return _json_response({"success": False}, status=400)
if req.method != "POST":
return _json_response({"success": False}, status=400)
if not body.get("teamId") or not body.get("membershipId"):
return _json_response({"success": False}, status=400)
success = onboard_user_to_team_workflow(
headers=req.headers,
body=body,
connhex_domain=CONNHEX_DOMAIN.value,
)
if not success:
return _json_response({"success": False}, status=403)
return _json_response({"success": True}, status=200)
except UnauthorizedError as exc:
logging.info("Bearer token authorization failed: %s", exc)
return _json_response(
{"success": False, "error": "missing_or_invalid_bearer_token"},
status=401,
)
except Exception:
logging.exception("Unexpected error occurred during Connhex team onboarding.")
return _json_response({"success": False}, status=500)The function itself stays small. It validates the HTTP request, delegates the Connhex workflow to another function, then maps the result to an HTTP response.
The important detail is that the Connhex domain comes from Firebase configuration:
from firebase_functions.params import StringParam
CONNHEX_DOMAIN = StringParam("CONNHEX_DOMAIN")In the function, the configured value is read with CONNHEX_DOMAIN.value.
Checking Connhex permissions
The core of the integration is the permission check.
Before calling the Connhex APIs that modify team membership, the Firebase function defines the exact Connhex permission pairs required by the operation:
from connhex.authz import PermissionPair
def required_pairs_for_team_onboarding(team_id: str) -> list[PermissionPair]:
resource = f"iam:teams:{team_id}"
return [
PermissionPair(resource=resource, action="iam:teams:get"),
PermissionPair(resource=resource, action="iam:teamUsers:list"),
PermissionPair(resource=resource, action="iam:teamUsers:create"),
]These pairs describe the resource and actions the caller must be allowed to use:
- read the target team
- list the current team users
- add a new team user
The Firebase function then sends those pairs to Connhex IAM using the caller’s bearer token:
from connhex.authz import introspect_pairs
def has_required_pairs(
*,
connhex_domain: str,
bearer_token: str,
pairs: list[PermissionPair],
) -> bool:
allowed_pairs = introspect_pairs(
connhex_domain=connhex_domain,
bearer_token=bearer_token,
pairs=pairs,
)
return all(pair in allowed_pairs for pair in pairs)If Connhex IAM does not return all the required pairs, the function returns 403. The operation stops before any write call is made.
This is the main pattern to reuse in other integrations: define the permissions required by your custom operation, introspect them with Connhex, and only then call the Connhex APIs that implement the operation.
Running the team onboarding workflow
After the authorization check succeeds, the rest of the function is normal server-side integration code.
The workflow extracts the input, reads the current team state and adds the membership if needed:
from typing import Any, Mapping
from connhex.authz import extract_bearer_token
from connhex.teams import add_team_users, get_team, list_team_users
def onboard_user_to_team_workflow(
*,
headers: Mapping[str, Any],
body: Mapping[str, Any],
connhex_domain: str,
) -> bool:
team_id = body["teamId"].strip()
membership_id = body["membershipId"].strip()
bearer_token = extract_bearer_token(headers)
has_permission = has_required_pairs(
connhex_domain=connhex_domain,
bearer_token=bearer_token,
pairs=required_pairs_for_team_onboarding(team_id),
)
if not has_permission:
return False
get_team(connhex_domain=connhex_domain, bearer_token=bearer_token, team_id=team_id)
existing_members = list_team_users(
connhex_domain=connhex_domain,
bearer_token=bearer_token,
team_id=team_id,
)
if membership_id not in existing_members:
add_team_users(
connhex_domain=connhex_domain,
bearer_token=bearer_token,
team_id=team_id,
membership_ids=[membership_id],
)
return TrueThis keeps the endpoint idempotent from the caller’s point of view. If the membership is already in the team, the function still returns success.
The same structure works for more complex integrations. A custom Firebase function can validate the input, check Connhex permissions, then combine several Connhex API calls into one business operation.
Configuration and deployment
The example project uses Firebase configuration for the Connhex domain:
from firebase_functions.params import StringParam
CONNHEX_DOMAIN = StringParam("CONNHEX_DOMAIN")For deployment, create functions/.env from the example file and set the Connhex domain for your instance. Unless you’re using an Enterprise version of Connhex, simply set:
CONNHEX_DOMAIN=connhex.comYou also need to configure the Firebase project used by the repository. In the example project, copy .firebaserc.example to .firebaserc and replace the placeholder project ID with your Firebase project ID.
Firebase Python functions also expect the deployment virtual environment to be available under functions/venv:
cd functions
python -m venv venv
venv/bin/pip install -r requirements.txtThen deploy the function with the Firebase CLI:
firebase deploy --only functions:onboard_user_to_teamWrapping up
Firebase Cloud Functions give Connhex users a simple place to build custom IoT backend integrations: HTTP endpoints, admin workflows, webhook handlers, scheduled jobs and application-specific API wrappers.
The key idea is to keep Connhex as the source of truth for IoT data, permissions and connected product APIs, while using Firebase for the project-specific code that connects Connhex to your application or business systems.
That pattern is useful beyond this team onboarding example. If you are building a customer portal, a field-service app, an internal operations dashboard or a reporting workflow, you can use the same split: let the IoT platform handle the infrastructure layer, then build the custom app logic where your product and business need it.
You can find the complete working example in the Connhex Firebase Examples repository.
