@hivedev/hivesdk
v1.0.50
Published
Software development kit for modules which integrate with the Hive system for educational institutions.
Readme
@hivedev/hivesdk
Software development kit for microservices that integrate with the Hive system — a hub-and-spoke platform for educational institutions.
Every service in the Hive ecosystem (NoteHive, and any future service) is built around this SDK. It handles authentication, configuration management, service registration, database connectivity, push notifications, and client-side session management — so each service only has to implement its own domain logic.
Table of Contents
- Concepts
- Installation
- ESM vs CJS — Which to Use
- Server SDK
- Client SDK
- Enumerations
- Environment Variables Reference
- How HivePortal Fits In
- Known Limitations
Concepts
HivePortal is the central hub. It owns all user accounts, sessions, permissions, and the service registry. Every microservice is a spoke — it delegates all authentication and configuration to HivePortal via this SDK.
Service registration is a two-step process:
- The service announces itself to HivePortal with its name and local/remote URLs.
- A HivePortal administrator approves it. Once approved, HivePortal sends back an encrypted configuration payload containing database credentials, Firebase credentials, permissions, and SMTP credentials.
Configuration is stored encrypted on disk (AES-256-CBC, key derived via scrypt from SERVER_PASSWORD). It is decrypted at startup and cached in process.env. Credentials never sit on disk in plaintext.
Authentication is always proxied. A service never validates a session token itself — it forwards the check to HivePortal and acts on the response.
Installation
npm install @hivedev/hivesdkThe SDK ships three pre-built files:
| File | Format | For |
|------|--------|-----|
| hive-server.js | ESM | Node.js backends with "type": "module" in package.json |
| hive-server.cjs | CJS | Node.js backends without "type": "module" |
| hive-client.js | ESM | Browser frontends only |
Peer dependencies:
nodemailerandmongodbare not bundled into the SDK. Install them in your service:npm install nodemailer mongodb
ESM vs CJS — Which to Use
The SDK provides both an ESM and a CJS build for the server. You use one or the other depending on your own application's module system — never both.
If your package.json has "type": "module", your project is ESM. Use import:
import { initServer } from "@hivedev/hivesdk/server";If your package.json does not have "type": "module" (or explicitly has "type": "commonjs"), your project is CJS. Use require:
const { initServer } = require("@hivedev/hivesdk/server");Mixing them will fail. Using require() in an ESM project throws:
ReferenceError: require is not defined in ES module scopeUsing import in a CJS project throws a syntax error. Check your package.json once and use the matching syntax everywhere.
All examples below show both variants. Use whichever matches your project.
Note: CJS does not support top-level
await. All async SDK calls in CJS must be wrapped in anasyncfunction or an async IIFE —(async () => { ... })(). The examples below show this where needed.
Server SDK
1. Initialising the Server
Call initServer once at the very start of your service, before anything else. It sets the service's identity and resolves the server password.
// ESM
import { initServer } from "@hivedev/hivesdk/server";
await initServer({
serviceName: "NoteHive", // Must match the name registered in HivePortal
servicePort: 49161, // The port this service listens on
remoteUrl: "", // Public-facing URL if accessible from outside LAN, empty if LAN-only
serverPassword: "" // Optional — see password resolution below
});// CJS
const { initServer } = require("@hivedev/hivesdk/server");
(async () =>
{
await initServer({
serviceName: "NoteHive",
servicePort: 49161,
remoteUrl: "",
serverPassword: ""
});
})();Password resolution order:
- The
serverPasswordargument, if provided. - The
SERVER_PASSWORDenvironment variable, if set. - An interactive terminal prompt — asks the user to create a password on first run, or enter their existing password on subsequent runs.
The password is used as the encryption key for all .dat configuration files. Losing it means losing access to the stored configuration.
Important:
initServermust only be called once. Calling it a second time is a no-op with a console warning. In a clustered setup, call it only from the primary process.
2. Registering the Service
After initServer, call registerService to announce this service to HivePortal and receive configuration.
// ESM
import { registerService } from "@hivedev/hivesdk/server";
await registerService();// CJS
const { registerService } = require("@hivedev/hivesdk/server");
await registerService(); // inside an async function or IIFEWhat this does internally:
- Sets the service's local URL (
http://<hostIp>:<servicePort>) and remote URL in the internal registry. - POSTs to HivePortal's
/RegisterServiceendpoint. - Begins polling HivePortal's
/Configurationendpoint every 2 seconds. - Once an administrator approves the service in HivePortal, the configuration payload is received, decrypted, and saved to disk.
On first run, the terminal will show:
Registering service...
Service registration hasn't been approved yet. Please contact administrators!
...
Configuration received.
Saving configuration...
Service registered.Once registered and approved, subsequent startups load configuration from disk directly and do not need to poll again.
In a clustered setup, call
registerServiceonly from the primary process, after a short delay to allow workers to start up first.
// ESM — clustered
import cluster from "cluster";
import os from "os";
import { initServer, registerService } from "@hivedev/hivesdk/server";
if (cluster.isPrimary)
{
await initServer({ serviceName: "NoteHive", servicePort: 49161, remoteUrl: "" });
setTimeout(async () => { await registerService(); }, 3000);
for (let i = 0; i < os.cpus().length; i++) cluster.fork({ ...process.env });
}// CJS — clustered
const cluster = require("cluster");
const os = require("os");
const { initServer, registerService } = require("@hivedev/hivesdk/server");
(async () =>
{
if (cluster.isPrimary)
{
await initServer({ serviceName: "NoteHive", servicePort: 49161, remoteUrl: "" });
setTimeout(async () => { await registerService(); }, 3000);
for (let i = 0; i < os.cpus().length; i++) cluster.fork({ ...process.env });
}
})();3. Mounting the Middleware
handleHiveRequests is an Express-compatible middleware that intercepts all Hive-related routes. Mount it before your own route handlers.
// ESM
import express from "express";
import cookieParser from "cookie-parser";
import { handleHiveRequests } from "@hivedev/hivesdk/server";
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use(handleHiveRequests); // Must come before your own routes// CJS
const express = require("express");
const cookieParser = require("cookie-parser");
const { handleHiveRequests } = require("@hivedev/hivesdk/server");
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use(handleHiveRequests);Routes handled automatically by this middleware:
All routes below transparently proxy to HivePortal. Each proxy injects the current serviceName and the caller's session token (read from cookie, x-session-token header, or body) before forwarding. HivePortal's status code and response body are passed through verbatim, so client-side code can rely on the same contract HivePortal documents.
| Method | Route | What it does |
|--------|-------|--------------|
| POST | /Login | Proxies credentials to HivePortal. On success sets the session cookie and/or x-session-token header. If the account is missing role-required fields, HivePortal returns { status: "missing_fields", missingFields, temporaryLoginToken } with status 200 and the SDK forwards that payload unchanged so the client can finish the flow via /SetMissingFields |
| POST | /Logout | Proxies logout to HivePortal |
| GET | /ServiceUrls | Returns local or remote URL for a named service. Caches discovery results in-process for 5 minutes per serviceName |
| POST | /IsLoggedIn | Checks if the current session is valid with HivePortal. Transparently rotates the session cookie / header when HivePortal signals a refresh |
| POST | /IsLoggedInWithPermission | Checks session validity and a named permission (request.body.permissionName) |
| POST | /SaveUserFilter | Saves a user-defined data filter (requires FILTER_OPERATIONS permission) |
| POST | /PushNotificationToken | Registers a push notification token for the current device against the current session |
| POST | /UserDetails | Returns { user, userFields } for the current session — the user record with password-format fields stripped, plus the role's field schema |
| POST | /UserSession | Returns the raw session object for the current session token (or null if invalid) |
| POST | /UserSubscriptions | Returns the current user's subscriptions. Pass subscriptionKey in the body to read a single sub-tree |
| GET | /UserFields | Returns the role/field schema configured in HivePortal. No auth required |
| POST | /UserInformationQuery | Runs a MongoDB-style query (request.body.mongoDbQuery) against the users collection. Requires a valid session |
| POST | /SetMissingFields | Submits the missing-field values from the login flow. Body: { temporaryLoginToken, fields }. On success sets the session cookie / header the same way /Login does |
| POST | /Register | Submits registration data. HivePortal sends an OTP to the user's email and stores an encrypted pending payload |
| POST | /VerifyRegisterOtp | Confirms the registration OTP and creates the user |
| POST | /RequestPasswordReset | Triggers a password-reset OTP email |
| POST | /VerifyPasswordResetOtp | Confirms the reset OTP and applies the new password |
| GET | /hive-client.js | Serves the built client-side SDK bundle from node_modules/@hivedev/hivesdk/hive-client.js |
You do not need to implement any of these routes yourself. Anything not in this list (admin/HivePortal-only routes such as /HivePortalLogin, /UpdateApproval, /PendingApprovals, /SetUserFields, /Configuration, /RegisterService, /PrivilegeNeeded, /GetNotificationTokens, /GetFiltersWhereUserIsIncluded, etc.) is intentionally not proxied — those either belong to HivePortal's own admin UI or are service-to-service endpoints that HivePortal gates by source IP.
4. Loading Configuration
On startup (after initServer), call loadConfiguration to decrypt and load credentials from disk into process.env.
// ESM
import { loadConfiguration } from "@hivedev/hivesdk/server";
await loadConfiguration();// CJS
const { loadConfiguration } = require("@hivedev/hivesdk/server");
await loadConfiguration(); // inside an async function or IIFEThis populates the following environment variables (only those with saved .dat files on disk are loaded):
| Variable | Contents |
|----------|----------|
| FIREBASE_ADMIN_CREDENTIALS | JSON string of Firebase Admin SDK credentials |
| DATABASE_CREDENTIALS | JSON string of database credentials (see shape note below) |
| PERMISSIONS | JSON string of permission definitions |
| SMTP_CREDENTIALS | JSON string of SMTP credentials |
| SMTP_PASSWORD | Extracted SMTP password, ready for nodemailer |
It also calls DatabaseConnector.setCredentials internally using the credentials for your service name, so the database is ready to connect after this call.
If a .dat file does not exist yet (e.g. first run before registration is approved), that credential is silently skipped.
Call
loadConfigurationbeforeregisterService. On first run the files won't exist yet and it will skip gracefully. Once the administrator approves the service andregisterServicesaves the config, it will be fully loaded on the next startup.
databaseCredentials shape
HivePortal stores databaseCredentials as a map keyed by service name ({ HivePortal: {...}, NoteHive: {...}, ... }), because the hub holds credentials for every registered service. When a service requests its configuration via /Configuration, HivePortal returns only that service's slice — a single { host, username, password, name } object, not the full map.
The SDK transparently handles both shapes:
loadConfigurationlooks upparsedDatabaseCredentials[serviceName]first, and falls back to treating the parsed value as the credentials object itself if that lookup is empty. So whether the on-disk blob is{ NoteHive: { host, ... } }(e.g. when running HivePortal itself) or{ host, ... }(e.g. when running a spoke service that received a slice),DatabaseConnector.setCredentialsends up with the right object.extractConfigurationForService()uses the same fallback when returningdatabaseCredentialsfromprocess.env.
You generally don't need to think about this — it just works. But if you're writing custom tooling around saveConfiguration, you can use either shape and reading it back will still resolve correctly.
5. Connecting to the Database
DatabaseConnector supports two connection modes. You use one or the other — not both at the same time.
Option A — MongoDB Atlas
Set the MONGODB_ATLAS_URL environment variable to your Atlas connection string. When this variable is present, connect() uses it directly and skips the local credentials system entirely — no setCredentials call, no loadConfiguration needed for the database.
The database name is read from the URL path if present (e.g. the mydb in ...mongodb.net/mydb). If the URL has no path, the name falls back to whatever was set via setCredentials, or "untitled" if neither was called.
// Set in your environment before connect() is called
process.env.MONGODB_ATLAS_URL = "mongodb+srv://user:[email protected]/mydb";// ESM
import DatabaseConnector from "./node_modules/@hivedev/hivesdk/DatabaseConnector.js";
const connected = await DatabaseConnector.connect();
if (!connected)
{
console.error("Failed to connect to database.");
process.exit(1);
}
const collection = DatabaseConnector.getDatabase().collection("my_collection");// CJS
const DatabaseConnector = require("./node_modules/@hivedev/hivesdk/DatabaseConnector.js");
(async () =>
{
const connected = await DatabaseConnector.connect();
if (!connected)
{
console.error("Failed to connect to database.");
process.exit(1);
}
const collection = DatabaseConnector.getDatabase().collection("my_collection");
})();Option B — Self-hosted / Local MongoDB
If MONGODB_ATLAS_URL is not set, connect() falls back to the local credentials flow. Credentials are set automatically by loadConfiguration using the service-name-keyed entry in the DATABASE_CREDENTIALS env var. If credentials have not been configured when connect() is called, it will attempt to call loadConfiguration itself.
// ESM — credentials are loaded automatically via loadConfiguration()
import DatabaseConnector from "./node_modules/@hivedev/hivesdk/DatabaseConnector.js";
const connected = await DatabaseConnector.connect();
if (!connected)
{
console.error("Failed to connect to database.");
process.exit(1);
}
const collection = DatabaseConnector.getDatabase().collection("my_collection");// CJS
const DatabaseConnector = require("./node_modules/@hivedev/hivesdk/DatabaseConnector.js");
(async () =>
{
const connected = await DatabaseConnector.connect();
if (!connected)
{
console.error("Failed to connect to database.");
process.exit(1);
}
const collection = DatabaseConnector.getDatabase().collection("my_collection");
})();Accessing the Database Object
Two equivalent ways to get the active database instance after a successful connect():
// Recommended — throws a clear error if not connected
const db = DatabaseConnector.getDatabase();
// Also works — direct field access, returns null if not connected
const db = DatabaseConnector.databaseObject;Disconnecting:
await DatabaseConnector.disconnect();6. Guarding Routes
Use isLoggedIn and isLoggedInWithPermission to protect your own route handlers. Both accept the request and response objects and a bSendResponse boolean.
When bSendResponse is false, the function returns a boolean and leaves the response untouched — use this to guard logic inside your own handlers.
When bSendResponse is true, the function writes the result directly to the response — used internally by handleHiveRequests.
Both functions handle session refresh transparently — if HivePortal signals that the token should be refreshed, a new cookie and/or x-session-token header is set automatically before your handler continues.
Login check:
// ESM
import { isLoggedIn } from "@hivedev/hivesdk/server";
app.get("/my-protected-route", async (request, response) =>
{
const loggedIn = await isLoggedIn(request, response, false);
if (!loggedIn)
{
response.statusCode = 401;
response.end("Unauthorized");
return;
}
response.end("Here is your protected data.");
});// CJS
const { isLoggedIn } = require("@hivedev/hivesdk/server");
app.get("/my-protected-route", async (request, response) =>
{
const loggedIn = await isLoggedIn(request, response, false);
if (!loggedIn)
{
response.statusCode = 401;
response.end("Unauthorized");
return;
}
response.end("Here is your protected data.");
});Permission check:
The permission name must be set on request.body.permissionName before calling isLoggedInWithPermission.
// ESM
import { isLoggedInWithPermission } from "@hivedev/hivesdk/server";
app.post("/admin-action", async (request, response) =>
{
request.body.permissionName = "ADMIN_OPERATIONS";
const permitted = await isLoggedInWithPermission(request, response, false);
if (!permitted)
{
response.statusCode = 403;
response.end("Forbidden");
return;
}
response.end("Admin action performed.");
});// CJS
const { isLoggedInWithPermission } = require("@hivedev/hivesdk/server");
app.post("/admin-action", async (request, response) =>
{
request.body.permissionName = "ADMIN_OPERATIONS";
const permitted = await isLoggedInWithPermission(request, response, false);
if (!permitted)
{
response.statusCode = 403;
response.end("Forbidden");
return;
}
response.end("Admin action performed.");
});7. Authenticating Cross-Service Requests
When one Hive service calls another on behalf of a logged-in user, the receiving service must verify that the user really is logged in at the calling service — sessions are scoped per-service. authenticateCrossServiceRequest proxies that check to HivePortal.
Wire contract — the calling service must include the following when it fetches your endpoint:
| Where | Field | Meaning |
|-------|-------|---------|
| Body | serviceName | The caller's service name (not yours) |
| Body or x-session-token header | sessionToken | The user's current session token |
| Header | x-device-id | The caller's device id |
The SDK function will read all three automatically. If your caller puts the requesting service name elsewhere (e.g. an x-service-name header, or a value you've already computed), pass it explicitly as the fourth argument and the body lookup is skipped.
// ESM
import { authenticateCrossServiceRequest } from "@hivedev/hivesdk/server";
app.post("/MyCrossServiceEndpoint", async (request, response) =>
{
const isAuthenticated = await authenticateCrossServiceRequest(request, response, false);
if(!isAuthenticated)
{
response.sendStatus(401);
return;
}
// ...service-to-service work on behalf of the user
});// CJS
const { authenticateCrossServiceRequest } = require("@hivedev/hivesdk/server");
app.post("/MyCrossServiceEndpoint", async (request, response) =>
{
const isAuthenticated = await authenticateCrossServiceRequest(request, response, false);
if(!isAuthenticated)
{
response.sendStatus(401);
return;
}
});Explicit service name override:
const requestingServiceName = request.headers["x-service-name"];
const isAuthenticated = await authenticateCrossServiceRequest(request, response, false, requestingServiceName);Signature:
authenticateCrossServiceRequest(request, response, bSendResponse = false, requestingServiceName = null) → Promise<boolean>bSendResponse = false(recommended) — returns the boolean and leaves the response object untouched so your handler can decide what to do.bSendResponse = true— writes the boolean directly as a JSON response.
Note: Unlike
isLoggedIn, this function does not rotate the session token. The contract is that the calling service refreshes its own session token before making the cross-service call.
Note: This is different from
isLoggedIn.isLoggedInvalidates the session against this service's name (read fromprocess.env.SERVICE_NAME).authenticateCrossServiceRequestvalidates the session against the calling service's name. Use the right one for the right context.
Fetching the user behind a cross-service request
authenticateCrossServiceRequest only tells you whether the session is valid. When you also need the user (e.g. to check user.role or user.privilege, or to write user._id into a record), use the companion helper getCrossServiceRequestUserDetails. It proxies HivePortal's /UserDetails using the same wire contract — same session token, same calling service name — and returns { user, userFields } for valid sessions or null for invalid ones.
// ESM
import { authenticateCrossServiceRequest, getCrossServiceRequestUserDetails } from "@hivedev/hivesdk/server";
app.post("/MyCrossServiceEndpoint", async (request, response) =>
{
if(!(await authenticateCrossServiceRequest(request, response, false)))
{
response.sendStatus(401);
return;
}
const userDetails = await getCrossServiceRequestUserDetails(request);
if(!userDetails)
{
response.sendStatus(401);
return;
}
const { user, userFields } = userDetails;
// user._id, user.role, user.privilege, plus any role-specific fields
// (password-format fields are stripped by HivePortal before being returned)
});user.role is a string; user.privilege is a number from the userPrivileges enum. userFields is the role-specific schema (useful if you want to render or validate the user object against the configured fields).
getCrossServiceRequestUserDetails(request, requestingServiceName = null) accepts the same explicit-service-name override as authenticateCrossServiceRequest. Returns null if the session is invalid, missing, or HivePortal is unreachable — a non-null return implies the session is valid, so you can use this in place of authenticateCrossServiceRequest when you need the user anyway.
8. Sending Mail
// ESM
import { sendMail } from "@hivedev/hivesdk/server";
await sendMail(
"[email protected]",
"[email protected]",
"Subject line",
"<p>HTML body</p>"
);// CJS
const { sendMail } = require("@hivedev/hivesdk/server");
await sendMail(
"[email protected]",
"[email protected]",
"Subject line",
"<p>HTML body</p>"
);Uses Gmail SMTP via nodemailer. The SMTP password is read from process.env.SMTP_PASSWORD, populated automatically by loadConfiguration. The sender Gmail address is hardcoded in SendMail.js — edit it in the SDK source if needed.
9. Utility Functions
getAppDataDirectory() — Platform-appropriate data directory for your service. On Windows: %APPDATA%\<ServiceName>, macOS: ~/Library/Application Support/<ServiceName>, Linux: ~/.config/<ServiceName>.
// ESM
import { getAppDataDirectory } from "@hivedev/hivesdk/server";
const dataDir = getAppDataDirectory(); // e.g. "/home/user/.config/NoteHive"// CJS
const { getAppDataDirectory } = require("@hivedev/hivesdk/server");
const dataDir = getAppDataDirectory();getHostIp() — First non-loopback IPv4 address of the host machine.
// ESM
import { getHostIp } from "@hivedev/hivesdk/server";
const ip = getHostIp(); // e.g. "192.168.1.42"// CJS
const { getHostIp } = require("@hivedev/hivesdk/server");
const ip = getHostIp();findService(serviceName) — Discovers another Hive service on the LAN via UDP to port 49153. Returns { name, urls: { local, remote } } or null. Requires HIVE_PORTAL_LAN_IP to be set.
// ESM
import { findService } from "@hivedev/hivesdk/server";
const service = await findService("NoteHive");
if (service) console.log(service.urls.local); // "http://192.168.1.42:49161"// CJS
const { findService } = require("@hivedev/hivesdk/server");
const service = await findService("NoteHive");
if (service) console.log(service.urls.local);extractConfigurationForService() — Parses process.env and returns the four credential sets as objects. Database credentials are extracted for the current service name specifically.
// ESM
import { extractConfigurationForService } from "@hivedev/hivesdk/server";
const { firebaseAdminCredentials, databaseCredentials, permissions, smtpCredentials } = extractConfigurationForService();// CJS
const { extractConfigurationForService } = require("@hivedev/hivesdk/server");
const { firebaseAdminCredentials, databaseCredentials, permissions, smtpCredentials } = extractConfigurationForService();encryptAndStoreFile(data, fullPath, password) — Encrypts a string or Buffer with AES-256-CBC and writes it to disk. Creates the directory if it doesn't exist.
// ESM
import { encryptAndStoreFile } from "@hivedev/hivesdk/server";
await encryptAndStoreFile("secret", "/path/to/file.dat", process.env.SERVER_PASSWORD);// CJS
const { encryptAndStoreFile } = require("@hivedev/hivesdk/server");
await encryptAndStoreFile("secret", "/path/to/file.dat", process.env.SERVER_PASSWORD);decryptFileWithPassword(fullPath, password) — Reads and decrypts an encrypted .dat file. Returns the decrypted string, or null on failure.
// ESM
import { decryptFileWithPassword } from "@hivedev/hivesdk/server";
const contents = await decryptFileWithPassword("/path/to/file.dat", process.env.SERVER_PASSWORD);// CJS
const { decryptFileWithPassword } = require("@hivedev/hivesdk/server");
const contents = await decryptFileWithPassword("/path/to/file.dat", process.env.SERVER_PASSWORD);saveConfiguration(configurationObject) / loadConfiguration() — Normally called automatically. saveConfiguration only writes keys present in the object — missing keys do not overwrite existing files.
// ESM
import { saveConfiguration, loadConfiguration } from "@hivedev/hivesdk/server";
await saveConfiguration({
databaseCredentials: { NoteHive: { host: "127.0.0.1", username: "admin", password: "pass", name: "notehive" } },
smtpCredentials: { password: "smtp-password" }
});
await loadConfiguration();// CJS
const { saveConfiguration, loadConfiguration } = require("@hivedev/hivesdk/server");
await saveConfiguration({
databaseCredentials: { NoteHive: { host: "127.0.0.1", username: "admin", password: "pass", name: "notehive" } },
smtpCredentials: { password: "smtp-password" }
});
await loadConfiguration();Full Server Bootstrap Example
// ESM
import express from "express";
import cookieParser from "cookie-parser";
import cluster from "cluster";
import os from "os";
import { handleHiveRequests, initServer, registerService, loadConfiguration } from "@hivedev/hivesdk/server";
import DatabaseConnector from "./node_modules/@hivedev/hivesdk/DatabaseConnector.js";
const SERVICE_NAME = "NoteHive";
const SERVICE_PORT = 49161;
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use(handleHiveRequests);
app.get("/", (request, response) => response.redirect("/Client/Pages/HomePage.html"));
if (cluster.isPrimary)
{
await initServer({
serviceName: SERVICE_NAME,
servicePort: SERVICE_PORT,
remoteUrl: process.env.REMOTE_URL || ""
});
await loadConfiguration();
setTimeout(async () =>
{
await registerService();
}, 3000);
for (let i = 0; i < os.cpus().length; i++)
{
cluster.fork({ ...process.env });
}
cluster.on("exit", (worker) =>
{
console.log(`Worker ${worker.process.pid} died. Restarting...`);
cluster.fork();
});
}
else
{
await DatabaseConnector.connect();
app.listen(SERVICE_PORT, () =>
{
console.log(`Worker ${process.pid} listening on port ${SERVICE_PORT}`);
});
}// CJS
const express = require("express");
const cookieParser = require("cookie-parser");
const cluster = require("cluster");
const os = require("os");
const { handleHiveRequests, initServer, registerService, loadConfiguration } = require("@hivedev/hivesdk/server");
const DatabaseConnector = require("./node_modules/@hivedev/hivesdk/DatabaseConnector.js");
const SERVICE_NAME = "NoteHive";
const SERVICE_PORT = 49161;
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use(handleHiveRequests);
app.get("/", (request, response) => response.redirect("/Client/Pages/HomePage.html"));
(async () =>
{
if (cluster.isPrimary)
{
await initServer({
serviceName: SERVICE_NAME,
servicePort: SERVICE_PORT,
remoteUrl: process.env.REMOTE_URL || ""
});
await loadConfiguration();
setTimeout(async () =>
{
await registerService();
}, 3000);
for (let i = 0; i < os.cpus().length; i++)
{
cluster.fork({ ...process.env });
}
cluster.on("exit", (worker) =>
{
console.log(`Worker ${worker.process.pid} died. Restarting...`);
cluster.fork();
});
}
else
{
await DatabaseConnector.connect();
app.listen(SERVICE_PORT, () =>
{
console.log(`Worker ${process.pid} listening on port ${SERVICE_PORT}`);
});
}
})();Client SDK
The client SDK is a browser-only ESM bundle. There is no CJS variant — it runs exclusively in the browser.
1. Loading the Client Bundle
The handleHiveRequests middleware automatically serves the client bundle at /hive-client.js by reading it from node_modules/@hivedev/hivesdk/hive-client.js. This works as long as your service's Node.js process is started from the root of the package (the directory containing node_modules), which is the recommended setup.
If your service is started from a different working directory, the automatic route will fail to locate the file. In that case, serve it manually:
import path from "path";
import { createReadStream } from "fs";
app.get("/hive-client.js", (request, response) =>
{
const filePath = "/absolute/path/to/node_modules/@hivedev/hivesdk/hive-client.js";
response.setHeader("Content-Type", "application/javascript");
createReadStream(filePath).pipe(response);
});Include it in your HTML pages:
<script type="module" src="/hive-client.js"></script>2. Initialising the Client
Call initClient once on page load. It sets up in-app WebView detection, signals cookie support to the server, and starts the periodic login state checker.
import { initClient } from "/hive-client.js";
await initClient({});| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| loginTokenStorageMethod | sessionStorageMethod enum | HTTP_ONLY_COOKIE | How session tokens are stored on the client |
3. Login
Call login() to open the HivePortal login page. The SDK handles the entire flow — the popup opens, the user enters their credentials, and on success the page redirects to /.
import { login } from "/hive-client.js";
login();Wire it to any element:
document.getElementById("login-button").addEventListener("click", login);When running inside the native mobile app (detected automatically), the login popup is rendered as a full-screen iframe overlay instead of a real popup window, since WebViews block window.open.
Missing-fields login flow
HivePortal's user schema is configured per-role (see /UserFields). If a user logs in but the schema requires fields the account doesn't have yet (for example, a new field was added after the user signed up), /Login responds with 200 and a body of:
{
"status": "missing_fields",
"missingFields": [ /* UserField definitions */ ],
"temporaryLoginToken": "..."
}No session is created in this case. The client must collect values for the listed fields and submit them to /SetMissingFields:
const response = await fetch("/SetMissingFields", {
method: "POST",
credentials: "include",
headers:
{
"Content-Type": "application/json",
"x-device-id": await getDeviceId()
},
body: JSON.stringify({
temporaryLoginToken,
fields: { fieldName1: "value", fieldName2: "value", ... }
})
});On success this endpoint creates the session and sets the session cookie / x-session-token header exactly like /Login would have, so the client can proceed normally afterwards. The temporary token has the same 300-second TTL as a registration OTP.
4. Checking Login State
initClient starts a periodic login check (every 30 seconds, clamped between 10 and 60 seconds). Each check fires a login-state-changed custom event on window.
window.addEventListener("login-state-changed", (event) =>
{
const isLoggedIn = event.detail;
document.getElementById("login-prompt").style.display = isLoggedIn ? "none" : "block";
document.getElementById("dashboard").style.display = isLoggedIn ? "block" : "none";
});Current login state is also available synchronously at any time:
if (window.IS_LOGGED_IN)
{
// Safe to load user-specific content
}Full Client Bootstrap Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>NoteHive</title>
</head>
<body>
<div id="login-prompt">
<button id="login-button">Sign In</button>
</div>
<div id="dashboard" style="display: none;">
<h1>Welcome to NoteHive</h1>
</div>
<script type="module">
import { initClient, login } from "/hive-client.js";
await initClient({});
window.addEventListener("login-state-changed", (event) =>
{
const isLoggedIn = event.detail;
document.getElementById("login-prompt").style.display = isLoggedIn ? "none" : "block";
document.getElementById("dashboard").style.display = isLoggedIn ? "block" : "none";
});
document.getElementById("login-button").addEventListener("click", login);
</script>
</body>
</html>Enumerations
userPrivileges — Numeric privilege levels assigned to users.
| Key | Value |
|-----|-------|
| STUDENT | 0 |
| CLASS_REPRESENTATIVE | 10 |
| STUDENT_COORDINATOR | 20 |
| ASSISTANT | 30 |
| TEACHER | 40 |
| COURSE_COORDINATOR | 50 |
| HEAD_OF_DEPARTMENT | 70 |
| MANAGEMENT | 80 |
| SUPER_USER | 100 |
sessionStorageMethod — How session tokens are stored on the client.
| Key | Value |
|-----|-------|
| HTTP_ONLY_COOKIE | 0 |
| LOCAL_STORAGE | 1 |
| SESSION_STORAGE | 2 |
clientConnectionTypeFlags — Bitmask flags describing how a client is connected.
| Key | Value |
|-----|-------|
| LAN | 1 |
| REMOTE | 2 |
| SAME_SYSTEM | 4 |
| UNKNOWN | 8 |
appWebViewInteractions — Message types exchanged between the web page and the native app WebView layer.
| Key | Value |
|-----|-------|
| REQUEST_NOTIFICATION_TOKEN | 0 |
| BACK_PRESSED | 1 |
approvalTypes — Types of approval requests sent to HivePortal administrators.
| Key | Value |
|-----|-------|
| USER_REGISTERATION | 0 |
| SERVICE_REGISTERATION | 1 |
filterTypes, filterOperators, userDataFetchFilterComparision, userFieldTypes, userFieldFormats, fileTreeNodeTypes — Domain-specific enumerations for data querying, user field definitions, and file tree structures. Refer to their respective source files for values.
Environment Variables Reference
| Variable | Set by | Description |
|----------|--------|-------------|
| SERVER_PASSWORD | External / setupPassword() | Master password for encrypting/decrypting config files |
| SERVICE_NAME | initServer | Name of this service as registered with HivePortal |
| SERVICE_PORT | initServer | Port this service listens on |
| REMOTE_URL | initServer | Public-facing URL of this service (empty if LAN-only) |
| HIVE_PORTAL_LAN_IP | External | LAN IP of HivePortal, used by findService for UDP discovery |
| MONGODB_ATLAS_URL | External | Atlas connection string — if set, bypasses local DB credentials entirely |
| FIREBASE_ADMIN_CREDENTIALS | loadConfiguration | JSON string of Firebase Admin credentials |
| DATABASE_CREDENTIALS | loadConfiguration | JSON string of DB credentials. Either a map keyed by service name (HivePortal's own format) or a single {host, username, password, name} object (the slice HivePortal sends to each spoke service). The SDK accepts both — see databaseCredentials shape |
| PERMISSIONS | loadConfiguration | JSON string of permission definitions |
| SMTP_CREDENTIALS | loadConfiguration | JSON string of SMTP credentials |
| SMTP_PASSWORD | loadConfiguration | Extracted SMTP password, ready for nodemailer |
How HivePortal Fits In
┌─────────────────────┐
│ HivePortal │
│ (hub — port 49152) │
│ │
│ - User accounts │
│ - Sessions │
│ - Permissions │
│ - Service registry │
│ - Configuration │
└────────┬─────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌──────────▼──────────┐ │ ┌──────────▼──────────┐
│ NoteHive │ ... │ FutureService │
│ (port 49161) │ │ (port 49162) │
│ │ │ │
│ uses @hivedev/ │ │ uses @hivedev/ │
│ hivesdk │ │ hivesdk │
└──────────────────────┘ └─────────────────────┘
Each service:
1. initServer() — establish identity and password
2. loadConfiguration() — decrypt credentials from disk
3. registerService() — announce to HivePortal, receive config
4. handleHiveRequests — mount middleware
5. Own domain logic — notes, files, etc.Every authentication check a service performs is a proxied call to HivePortal. HivePortal is the single source of truth for all session and permission data. Services only store and serve their own domain data.
Known Limitations
ScheduleServiceUrlUpdate— Currently a stub and does nothing. Intended for future use to re-broadcast service URLs if the host IP changes.loginTokenStorageMethod—LOCAL_STORAGEandSESSION_STORAGEare defined in the enum but not yet implemented. OnlyHTTP_ONLY_COOKIE(with thex-session-tokenheader fallback for native app clients) is fully functional.- SMTP sender address — Hardcoded in
SendMail.js. Edit it in the SDK source if your service needs a different address. - MongoDB only —
DatabaseConnectoris built for MongoDB only. - MongoDB port hardcoded — For local connections, the port is fixed at
27017. The host is configurable via credentials but the port is not. Use Atlas (MONGODB_ATLAS_URL) if you need a non-standard port.
