Skip to content

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.

ini
@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/dev Vite/test shim (Framework Packages)

Install with npm install @productifyfw/core.

typescript
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 proxySharedSecret that verifies X-Proxy-Auth before trusting identity headers (mirrors the Go library)
  • Backend registration with heartbeat, and trigger handling

Install with npm install @productifyfw/node-express.

typescript
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.

bash
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 adapter
go
package 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)):

go
import "github.com/ProductifyFW/be-integrations/go/managertest"

m := managertest.New(t) // register/heartbeat/config endpoints, scriptable failures

Communications ​

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.

ts
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.

ts
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-express

Frontend (Browser) ​

  1. Proxy injects window.__PRODUCTIFY__ into HTML
  2. Application loads and accesses via @productifyfw/core
  3. 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) ​

  1. Proxy forwards requests with authentication headers
  2. Express middleware extracts headers into req.productify
  3. Route handlers access authenticated context

Type Consistency ​

Both packages use types that map to the same proxy structures:

Proxy SourceFrontend TypeBackend Type
X-User-IDuser.iduser.id
X-User-Emailuser.emailuser.email
X-User-Nameuser.nameuser.name
X-User-Usernameuser.usernameuser.username
X-User-Picture ¹user.pictureuser.picture
X-Tenant-IDtenant.idtenant.id
X-Tenant-Nametenant.nametenant.name
X-User-Tenant-Role ¹tenantRoletenantRole
X-User-Tenant-Permissions ¹tenantPermissionstenantPermissions
X-Auth-TypeauthTypeauthType
Manager APIconfig-

¹ 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 ​

  1. Start the proxy with authentication enabled
  2. Configure the application ID in the proxy
  3. Add the middleware to the backend
  4. Read the context in the frontend and backend

Testing ​

Both packages include test suites.

Frontend:

bash
cd fe-integrations/packages/core
pnpm test

Backend:

bash
cd be-integrations/packages/node-express
pnpm test

Common Patterns ​

Full-Stack Type Safety ​

typescript
// 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 ​

typescript
// 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

Further Reading ​