Integration Libraries
These are the libraries you add to your own application so it can work behind the Productify proxy: read the signed-in user and tenant, load pilets, receive scheduled triggers from the Manager, and send notifications. There is a browser library (@productifyfw/core plus Vue and React bindings), a Node/Express server library, and a Go server library. The sections below give a short tour of each; the linked guides go deeper.
Installing from GitHub Packages
The npm packages are published to the GitHub Packages registry. Point the @productifyfw scope at it in .npmrc, and add a GitHub token; GitHub Packages requires authentication to install even public packages. The current frontend release is v1.1.2; the Node backend packages are @productifyfw/node-express 1.0.0 and @productifyfw/testing 0.2.0.
@productifyfw:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}See Frontend Integration or Backend Integration for details.
Frontend Integration
@productifyfw/core
TypeScript client library for the Productify configuration the proxy injects. It provides:
- Type-safe access to
window.__PRODUCTIFY__ - User, tenant, and application context
- Configuration management with defaults
- Pilet loading system - Dynamic ES module loading from a feed service, with each bundle's SHA-256 hash and Ed25519 signature verified before execution
- Framework bindings ship as their own packages:
@productifyfw/vue,@productifyfw/react, and the@productifyfw/devVite/test shim (Framework Packages)
Install with npm install @productifyfw/core.
import { ProductifyClient, loadPilets } from "@productifyfw/core";
import { reactive } from "vue";
const pfy = new ProductifyClient();
// User and tenant context
const user = pfy.getUser();
const tenant = pfy.getTenant();
// Standard configuration keys
const theme = pfy.getDefaultTheme() ?? "light";
// Your own settings arrive as key/value pairs (string values)
const kv = Object.fromEntries(pfy.getKeyValuePairs().map((p) => [p.key, p.value]));
const maxItems = Number(kv.maxItemsPerPage ?? 10);
// Modules (Pilets)
const enabledModules = pfy.getEnabledModules();
if (pfy.isModuleEnabled("dashboard")) {
// Load dashboard module
}
// Load pilets dynamically
const piletRegistry = reactive({ extensions: {}, pages: {} });
loadPilets("/pilet-feed", piletRegistry);
// Themes
const enabledThemes = pfy.getEnabledThemes();
const defaultTheme = pfy.getDefaultTheme();
// Locales
const enabledLocales = pfy.getEnabledLocales();
const defaultLocale = pfy.getDefaultLocale();Vue 3 + Pilet Loading Example
Example application demonstrating microfrontend architecture with Vue 3, ES module pilets, and @productifyfw/core. It shows:
- Vue 3 shell application with Composition API
- Dynamic ES module pilet loading from feed service
- Custom pilet API (Piral-compatible without framework dependency)
- Example pilet implementations with dashboard widgets
- Productify proxy integration
- CLI pilet upload support with PAT authentication
See the Vue 3 + Pilet Loading Example Guide. The example is published as the image ghcr.io/productifyfw/example-frontend:0.1.0; the Node example backend below is ghcr.io/productifyfw/example-app:0.1.0. Both are public, and the Compose bundle starts them with --profile example.
Backend Integration
A server library for Node.js/Express and a Go library. The wire protocol (identity headers, registration + heartbeat, trigger callbacks) is shared and held byte-identical by a parity test suite (go/parity_test.go), and each library ships a fake-Manager test double (@productifyfw/testing for Node, Go managertest).
@productifyfw/node-express
Express.js middleware for the Productify authentication headers. It provides:
- Extract user/tenant from HTTP headers
- Type-safe request context via
req.productify - Authentication middleware with an optional
proxySharedSecretthat verifiesX-Proxy-Authbefore trusting identity headers (mirrors the Go library) - Backend registration with heartbeat, and trigger handling
Install with npm install @productifyfw/node-express.
import express from "express";
import {
productifyMiddleware,
requireProductify,
} from "@productifyfw/node-express";
const app = express();
app.use(productifyMiddleware({ applicationId: "my-app" }));
app.get("/api/profile", requireProductify, (req, res) => {
const { user, tenant } = req.productify!;
res.json({ user, tenant });
});Go library (be-integrations/go)
A dependency-free net/http core with a Gin adapter module: the same header contract, registration lifecycle (heartbeat, re-register on 404, graceful unregister), timing-safe trigger handling, and per-tenant config client as the Node library. The current release tags are go/v0.2.0 and go/gin/v0.2.0.
go get github.com/ProductifyFW/be-integrations/go@v0.2.0 # core (net/http)
go get github.com/ProductifyFW/be-integrations/go/gin@v0.2.0 # Gin adapterpackage main
import (
"context"
"encoding/json"
"net/http"
"os"
productify "github.com/ProductifyFW/be-integrations/go"
)
func main() {
mw := productify.Middleware(productify.MiddlewareConfig{
ProxySharedSecret: os.Getenv("PROXY_SHARED_SECRET"), // strict auto-enables when set
})
mux := http.NewServeMux()
mux.HandleFunc("/api/profile", func(w http.ResponseWriter, r *http.Request) {
ctx, _ := productify.FromContext(r.Context())
json.NewEncoder(w).Encode(map[string]any{
"user": ctx.User, "tenant": ctx.Tenant,
})
})
backend, err := productify.Register(context.Background(), productify.RegisterConfig{
ManagerURL: os.Getenv("MANAGER_URL"),
MachineUserToken: os.Getenv("MACHINE_USER_TOKEN"),
ApplicationID: os.Getenv("APPLICATION_ID"),
CallbackURL: os.Getenv("CALLBACK_URL"),
})
if err == nil {
defer backend.Close(context.Background())
} // registration failure is non-fatal: serve traffic, retry in background
http.ListenAndServe(":8080", mw(mux))
}Testing against a fake Manager (inside a func TestX(t *testing.T)):
import "github.com/ProductifyFW/be-integrations/go/managertest"
m := managertest.New(t) // register/heartbeat/config endpoints, scriptable failuresCommunications
Notifications and live data updates, shared by the backend and frontend SDKs.
Add Notifications to Your App
Durable, per-user notifications: your backend emits, a bell lights up over the live socket, and an unread notification falls back to email once the user's delivery window is open.
await comms.notifications.emit(
{ userIds: [order.customerId], tenantId: order.tenantId },
{ kind: "order.shipped", title: "Your order shipped" },
);Live Data Updates
A lossy, at-most-once hint channel for "something you are looking at changed". Read the D7 contract before publishing anything: payloads are visible to every tenant member with access to the app.
comms.subscribe("orders", () => refetchOrders());Architecture
Data Flow
┌─────────┐ ┌───────┐ ┌─────────┐ ┌─────────┐
│ Client │─────▶│ Proxy │─────▶│ Backend │─────▶│ Manager │
└─────────┘ └───────┘ └─────────┘ └─────────┘
│ │ │ │
│ Auth Token X-Headers Config
│ │ │ API
│ ▼ │ │
│ Validate Extract Fetch
│ │ │ │
│ ▼ ▼ ▼
│ User/Tenant req.productify /frontend-config
│ │ │
│ ▼ │
└──────▶ window.__PRODUCTIFY__ │
│ │
▼ ▼
@productifyfw/core @productifyfw/node-expressFrontend (Browser)
- Proxy injects
window.__PRODUCTIFY__into HTML - Application loads and accesses via
@productifyfw/core - Type-safe access to user, tenant, and configuration
Custom settings are key/value pairs, not top-level config keys
The Manager delivers your own settings as config.key_value_pairs: [{ key, value }] with string values. pfy.getConfig(key), isConfigEnabled(key) and getNestedConfig(path) read top-level keys of config (such as default_theme), so they return null/false for a key that only exists in key_value_pairs. Read custom settings with pfy.getKeyValuePairs() and convert the string yourself.
Backend (Server)
- Proxy forwards requests with authentication headers
- Express middleware extracts headers into
req.productify - Route handlers access authenticated context
Type Consistency
Both packages use types that map to the same proxy structures:
| Proxy Source | Frontend Type | Backend Type |
|---|---|---|
| X-User-ID | user.id | user.id |
| X-User-Email | user.email | user.email |
| X-User-Name | user.name | user.name |
| X-User-Username | user.username | user.username |
| X-User-Picture ¹ | user.picture | user.picture |
| X-Tenant-ID | tenant.id | tenant.id |
| X-Tenant-Name | tenant.name | tenant.name |
| X-User-Tenant-Role ¹ | tenantRole | tenantRole |
| X-User-Tenant-Permissions ¹ | tenantPermissions | tenantPermissions |
| X-Auth-Type | authType | authType |
| Manager API | config | - |
¹ Opt-in per application (forward_role setting, default off). X-User-Tenant-Role is the membership role name (project-configurable, not a stable contract); gate on X-User-Tenant-Permissions (fixed vocabulary) instead. window.__PRODUCTIFY__ also exposes available_tenants: [{id, name}] for building an in-app tenant switcher. All three opt-in headers are on the proxy's sanitizer strip list. See Access Model and Multi-tenancy.
Security Considerations
Frontend
- Configuration is injected by trusted proxy
- No sensitive data should be in frontend config
- Use for UI preferences and feature flags only
Backend
- Headers are injected after authentication
- Headers can be trusted - they come from validated tokens
- Always validate permissions for actions
- Never rely on client-side checks alone
- Implement tenant isolation in database queries
Development Workflow
Setting Up Local Development
- Start the proxy with authentication enabled
- Configure the application ID in the proxy
- Add the middleware to the backend
- Read the context in the frontend and backend
Testing
Both packages include test suites.
Frontend:
cd fe-integrations/packages/core
pnpm testBackend:
cd be-integrations/packages/node-express
pnpm testCommon Patterns
Full-Stack Type Safety
// Frontend: read a custom setting from key_value_pairs
const kv = Object.fromEntries(pfy.getKeyValuePairs().map((p) => [p.key, p.value]));
const theme = kv.theme === "dark" ? "dark" : "light";
// Backend
const { user, tenant } = req.productify!;Permission Checking
// Frontend (UI only): key_value_pairs values are strings
const kv = Object.fromEntries(pfy.getKeyValuePairs().map((p) => [p.key, p.value]));
if (kv["features.admin"] === "true") {
// Show admin UI
}
// Backend (actual permission check)
app.get("/api/admin", requireProductify, async (req, res) => {
const hasPermission = await checkPermission(req.productify!.user.id, "admin");
if (!hasPermission) {
return res.status(403).json({ error: "Forbidden" });
}
// Process admin request
});Monorepo Structure
Both integration libraries use pnpm workspaces for monorepo management:
fe-integrations/
└── packages/
├── core/ # @productifyfw/core (framework-agnostic)
├── vue/ # @productifyfw/vue
├── react/ # @productifyfw/react
├── dev/ # @productifyfw/dev (Vite shim + test helper)
├── comms-pilet/ # @productifyfw/comms-pilet (bell + inbox, Vue)
└── vue3-example/ # example host app, not a published binding
be-integrations/
├── packages/
│ ├── node-express/ # @productifyfw/node-express
│ ├── testing/ # @productifyfw/testing (FakeManager)
│ └── node-express-example/
└── go/ # github.com/ProductifyFW/be-integrations/go
├── gin/ # Gin adapter submodule
└── managertest/ # in-process fake Manager