Frontend Integration Guide
The proxy puts a configuration object into window.__PRODUCTIFY__ for every browser application it serves; @productifyfw/core, @productifyfw/vue and @productifyfw/react read it. The core library guide has the full API and the pilet-verification details.
Table of Contents
- Overview
- Installation
- Configuration Injection
- Library Usage
- Framework Integration
- Module Loading
- Security Considerations
Overview
The Productify frontend integration consists of two main parts:
- The proxy injects configuration data into
window.__PRODUCTIFY__. @productifyfw/corereads it through typed accessors;@productifyfw/vueand@productifyfw/reactbind it to the framework, and@productifyfw/devis the Vite dev shim. See Framework Packages.
Installation
Configure npm to resolve @productifyfw packages from GitHub Packages by adding to .npmrc (a GitHub token is required to install from that registry):
@productifyfw:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}Then install:
npm install @productifyfw/coreOr with other package managers:
yarn add @productifyfw/core
pnpm add @productifyfw/coreAdd the framework binding you need; both layer over the same core client:
npm install @productifyfw/vue # or @productifyfw/react
npm install -D @productifyfw/dev # Vite dev shim + test helperConfiguration Injection
How It Works
When your application is served through the Productify proxy, the HTML response is automatically modified to include a <script> tag that defines window.__PRODUCTIFY__:
<script id="productify-config" type="application/json">
{
"user": {
"id": "user-uuid",
"email": "user@example.com",
"name": "User Name",
"username": "username"
},
"tenant": {
"id": "tenant-uuid",
"name": "Tenant Name"
},
"available_tenants": [
{ "id": "tenant-uuid", "name": "Tenant Name" },
{ "id": "tenant-uuid-2", "name": "Other Org" }
],
"application": {
"id": "application-uuid"
},
"auth_type": "oauth",
"logout_url": "/_tenant/logout",
"config": {
"enabled_modules": ["example-pilet@1.0.5"],
"default_theme": "light",
"default_locale": "en",
"pilet_signing_keys": { "prod-2026-01": "<base64 raw Ed25519 public key>" },
"communications": { "enabled": true },
"key_value_pairs": [{ "key": "maxItemsPerPage", "value": "25" }]
}
}
</script>
<script>
window.__PRODUCTIFY__ = JSON.parse(
document.getElementById("productify-config").textContent
);
</script>The injection is placed before your application JavaScript loads, so the configuration is available immediately.
config is the merged configuration for the tenant named in tenant.id, fetched by the proxy from GET /api/frontend-config/:application_id/:tenant_id. On a dedicated vhost that tenant is the pinned one; on a shared vhost it is the tenant the user selected, so tenant and config always describe the same tenant. When a shared vhost has no tenant resolved yet, config carries project and application defaults only.
Configuration Structure
interface ProductifyFrontendConfig {
// Current authenticated user (from X-User-* headers)
user: {
id: string;
email: string;
name: string;
username: string;
picture?: string; // opt-in: X-User-Picture (forward_role)
};
// Current tenant (from X-Tenant-* headers)
tenant: {
id: string;
name: string;
};
// Tenants this user may act in for this app (id + name only).
// Render your own in-app switcher; POST /_tenant to switch.
available_tenants?: Array<{ id: string; name: string }>;
// Opt-in membership context (forward_role, default off).
// Gate on tenantPermissions (fixed vocabulary), not tenantRole (project-configurable name).
tenantRole?: string;
tenantPermissions?: string[];
// Current application (configured in the proxy)
application: {
id: string;
};
// Authentication type (from X-Auth-Type header). The proxy emits the
// snake_case `auth_type`; `authType` is the canonical key. Both are declared,
// and ProductifyClient.getAuthType() reads authType first, then auth_type.
// See /known-issues#auth-type-wire-key.
authType?: string;
auth_type?: string;
// Standard "Sign out" target. Read it through pfy.getLogoutUrl() (or the
// useLogoutUrl() hook/composable), which substitutes the proxy default
// /_tenant/logout when the key is absent, so logout always clears both the
// portal session and the tenant cookie.
logout_url?: string;
// Frontend configuration fetched from the Manager
// (/api/frontend-config/:application_id/:tenant_id)
config?: {
available_modules?: string[]; // format: name@version
enabled_modules?: string[]; // format: name@version
available_themes?: string[];
enabled_themes?: string[];
default_theme?: string;
available_locales?: string[];
enabled_locales?: string[];
default_locale?: string;
// Pilet provenance trust anchor: keyId -> base64 raw Ed25519 public key.
// Its presence marks a verification-enforcing deployment.
pilet_signing_keys?: Record<string, string>;
// Whether communications are on for this (tenant, application).
communications?: { enabled: boolean };
// Per-tenant pilet version pins from the config cascade: name -> version.
pilets?: { pins?: Record<string, string> };
// Feed URL published by `pfy dev proxy --pilet-bundle`; honoured by
// loadPilets only when it is same-origin. Absent in production.
pilet_feed?: string;
key_value_pairs?: Array<{ key: string; value: string }>;
[key: string]: unknown;
};
}Library Usage
Basic Setup
import { ProductifyClient } from "@productifyfw/core";
const pfy = new ProductifyClient();
// Check if configuration is available
if (!pfy.isConfigured()) {
console.warn("Productify configuration not found");
}Accessing Configuration
// Application and tenant identifiers
const appId = pfy.getApplicationId();
const tenantId = pfy.getTenantId();
const authType = pfy.getAuthType();
// User information
const user = pfy.getUser();
const userId = pfy.getUserId();
const userEmail = pfy.getUserEmail();
// Sign-out target (falls back to /_tenant/logout when the proxy injected none)
signOutLink.href = pfy.getLogoutUrl();
// Your own settings: key_value_pairs, string values
const kv = Object.fromEntries(pfy.getKeyValuePairs().map((p) => [p.key, p.value]));
const maxItems = Number(kv.maxItemsPerPage ?? 10);
const featureEnabled = kv.newCheckout === "true";
// Top-level config keys (the standard ones, or anything you inject yourself)
const defaultTheme = pfy.getConfig<string>("default_theme", { defaultValue: "light" });
const mode = pfy.getNestedConfig<string>("ui.theme.mode"); // only if `config.ui` existsgetConfig reads top-level config keys, not key_value_pairs
The Manager delivers the settings you define as config.key_value_pairs: [{ key, value }] with string values. getConfig(key), isConfigEnabled(key) and getNestedConfig(path) look up top-level keys of config and therefore return null/false for those settings. Use getKeyValuePairs() for your own settings, as above.
Themes and Locales
// Themes
const enabledThemes = pfy.getEnabledThemes();
const defaultTheme = pfy.getDefaultTheme();
// Locales
const enabledLocales = pfy.getEnabledLocales();
const defaultLocale = pfy.getDefaultLocale();
if (pfy.isLocaleEnabled("hu")) {
// Offer Hungarian in the language switcher
}Module Management
// List modules (format: name@version)
const enabled = pfy.getEnabledModules();
const available = pfy.getAvailableModules();
// Check if a module is enabled (matches the name segment exactly)
if (pfy.isModuleEnabled("checkout")) {
console.log("Checkout module enabled");
}
// Check a specific version
if (pfy.isModuleEnabled("analytics@2.0.0")) {
// Use new analytics features
}Framework Integration
Vue 3
@productifyfw/vue provides the plugin, the useProductify() composable over the core client, useTenant(), useLogoutUrl(), the i18n composable and the Vue pilet glue.
Plugin Setup
The export is productifyPlugin (lowercase p), and it is a plugin object, not a factory: pass it to app.use() directly, with options as the second argument:
// main.ts
import { createApp } from "vue";
import { productifyPlugin } from "@productifyfw/vue";
import App from "./App.vue";
const app = createApp(App);
app.use(productifyPlugin);
// or: app.use(productifyPlugin, { strict: true });
app.mount("#app");Composition API Usage
<script setup lang="ts">
import { computed } from "vue";
import { useProductify } from "@productifyfw/vue";
// useProductify() returns the ProductifyClient itself.
const pfy = useProductify();
const userName = computed(() => pfy.getUserName() ?? "Guest");
const maxItems = computed(
() => pfy.getConfig<number>("maxItemsPerPage", { defaultValue: 10 }) ?? 10
);
const showBeta = computed(() => pfy.isConfigEnabled("betaFeatures"));
</script>
<template>
<div>
<h1>Welcome, {{ userName }}</h1>
<BetaPanel v-if="showBeta" />
<ItemList :max-items="maxItems" />
</div>
</template>React
@productifyfw/react provides a context provider and hooks over the core client. useProductify() returns the ProductifyClient itself; useUser(), useTenant(), useTenants(), useLogoutUrl(), useProductifyConfig(), useLocale() and usePilets() return values that re-render when the injected config changes.
App Setup
// App.tsx
import { ProductifyProvider } from "@productifyfw/react";
import Dashboard from "./Dashboard";
function App() {
return (
<ProductifyProvider>
<Dashboard />
</ProductifyProvider>
);
}
export default App;Component Usage
// Dashboard.tsx
import {
useProductify,
useProductifyConfig,
useUser,
} from "@productifyfw/react";
function Dashboard() {
const pfy = useProductify();
const user = useUser();
const maxItems = useProductifyConfig<number>("maxItemsPerPage", {
defaultValue: 10,
});
const showAnalytics = pfy.isModuleEnabled("analytics");
return (
<div>
<h1>Welcome, {user?.name ?? "Guest"}</h1>
{showAnalytics && <AnalyticsDashboard />}
<ItemList maxItems={maxItems ?? 10} />
</div>
);
}
export default Dashboard;Other Frameworks
@productifyfw/core is framework-agnostic: instantiate ProductifyClient (or use the getProductifyClient() singleton) in any framework's dependency-injection mechanism. Dedicated bindings currently exist for Vue 3 and React only.
Module Loading
Productify supports dynamic module (pilet) loading. Enabled modules are listed in the frontend configuration and served by the Manager's pilet feed.
Module Configuration
Modules are configured in the Manager and included in window.__PRODUCTIFY__.config:
window.__PRODUCTIFY__.config.enabled_modules = [
"navigation@1.2.3",
"checkout@2.0.1",
"analytics@0.5.0",
];Loading Pilets
import { loadPilets, type PiletRegistry } from "@productifyfw/core";
const registry: PiletRegistry = { extensions: {}, pages: {} };
await loadPilets("/pilet-feed", registry, {
onLoad: (pilet) => console.log(`Loaded ${pilet.name}@${pilet.version}`),
onError: (pilet, error) => console.error(`Failed ${pilet.name}`, error),
});Each bundle's SHA-256 hash and its detached Ed25519 signature are verified against the injected trust anchor before it is executed; a refusal is reported through onError and nowhere else, so always pass that callback. Vue and React callers should prefer loadVuePilets / loadReactPilets, which inject their framework's registration adapter. See the frontend integration library guide for the full verification model.
When the injected config carries config.pilet_feed (written by pfy dev proxy --pilet-bundle), loadPilets fetches that feed instead of the URL argument, provided it resolves to the page's own origin; a cross-origin value is ignored with a console warning. Pass useInjectedFeed: false to always use the argument. Verification is unaffected: bundles from the injected feed must still be signed by a key from the trust anchor.
Bundle fetches go to the Manager, not to your SPA
A Manager-rendered application vhost serves /pilet-feed, /pilets/* and /language-packs/* from the Manager, ahead of the SPA. If a bundle fetch comes back as index.html with a 200 on a hand-written vhost, the SPA fallback is answering for /pilets/* and the integrity check fails with what looks like a signing error; route those paths to the Manager as the rendered config does.
Security Considerations
Trusted Data
The data in window.__PRODUCTIFY__ comes from the Productify proxy after authentication and authorization. This data can be trusted for:
- Displaying user information
- Controlling UI visibility based on configuration
- Configuring application behavior
Client-Side Only
Never rely solely on client-side checks for security. Always validate permissions on the backend.
const kv = Object.fromEntries(pfy.getKeyValuePairs().map((p) => [p.key, p.value]));
// [OK] Good: Hide UI elements
if (kv.adminUi !== "true") {
// Don't show admin button
}
// [NO] Bad: Only client-side protection
if (kv.adminUi === "true") {
// Make admin API call
// Backend MUST also check permissions!
}XSS Protection
The library does not execute any configuration values. All data is treated as plain data. However:
- Never use
innerHTMLwith configuration values - Sanitize any user-controlled content before rendering
- Use framework's built-in XSS protection (Vue templates, React JSX, etc.)
Best Practices
- Create the
ProductifyClientat app startup and share one instance (thegetProductifyClient()singleton) across the application. - Provide a default for every configuration value you read.
- Cache expensive checks in component state or computed properties.
- Handle a missing configuration gracefully; the client does not throw unless constructed with
strict: true. - Never modify
window.__PRODUCTIFY__directly.
Troubleshooting
Configuration Not Found
If window.__PRODUCTIFY__ is undefined:
- Verify application is accessed through Productify proxy
- Check proxy configuration and middleware
- Ensure correct domain/URL is used
- Check browser console for injection errors
Type Errors
Ensure TypeScript configuration includes:
{
"compilerOptions": {
"lib": ["ES2020", "DOM"],
"moduleResolution": "bundler"
}
}Stale Configuration
If configuration doesn't update:
// Manually refresh configuration; onChange fires when content changed
const pfy = new ProductifyClient({
onChange: (config) => console.log("Configuration updated:", config),
});
pfy.refresh();Related
- Frontend Integration - @productifyfw/core - full library reference, pilet verification, i18n
- Frontend Library Usage Examples - copy-paste patterns for Vue and React
- Vue 3 + Pilet Loading Example - a complete host application