Frontend Integration - @productifyfw/core
Complete guide for integrating Productify configuration access and pilet loading in your applications.
Table of Contents
- Overview
- Installation
- Quick Start
- Configuration Structure
- Pilet Loading
- API Reference
- Framework Integration
- Testing
- Best Practices
- Troubleshooting
Overview
The @productifyfw/core library provides:
- Type-safe configuration access - Access to
window.__PRODUCTIFY__configuration injected by the proxy - Pilet loading - Dynamic loading of ES module pilets from feed service
- Framework agnostic - Works with Vue, React, or vanilla JavaScript
The types are directly derived from the Go struct definitions in proxy/caddy/productify/html_injection_middleware.go, ensuring complete type safety and compatibility.
How It Works
The Productify proxy (Caddy middleware) intercepts HTML responses and injects configuration before the closing </head> tag:
User/Tenant Data: Extracted from HTTP headers set by authentication middleware:
X-User-ID,X-User-Email,X-User-Name,X-User-UsernameX-Tenant-ID,X-Tenant-NameX-Auth-Type
Application Config: Fetched from Manager API endpoint
/api/frontend-config/:application_idInjection: Combined into
window.__PRODUCTIFY__object before your application loadsPilet Loading: Feed service returns enabled pilets which are dynamically imported as ES modules
This means the configuration is available immediately when your JavaScript executes, with no additional API calls needed.
Installation
From GitHub Packages (Public)
ProductifyFW packages are published as public packages on GitHub Packages. Configure npm to use the GitHub registry for @productifyfw scoped packages:
Create or update .npmrc in your project root:
@productifyfw:registry=https://npm.pkg.github.comThen install:
npm install @productifyfw/core
# or
pnpm add @productifyfw/core
# or
yarn add @productifyfw/coreNo Authentication Required
Public packages don't require a GitHub token for installation. The .npmrc configuration just tells npm where to find @productifyfw packages.
Quick Start
Configuration Access
import { ProductifyClient } from "@productifyfw/core";
const pfy = new ProductifyClient();
// Get user information
const user = pfy.getUser();
console.log(`Welcome, ${user?.name}!`);
// Get configuration values
const theme = pfy.getConfig<string>("theme", { defaultValue: "light" });
const maxItems = pfy.getConfig<number>("maxItemsPerPage", { defaultValue: 10 });
// Get tenant information
const tenantId = pfy.getTenantId();
const tenantName = pfy.getTenantName();Pilet Loading
import { loadPilets, type PiletRegistry } from "@productifyfw/core";
import { reactive } from "vue";
// Create reactive registry
const piletRegistry: PiletRegistry = reactive({
extensions: {},
pages: {},
});
// Load pilets from feed
await loadPilets("/pilet-feed", piletRegistry, {
onLoad: (pilet) => console.log(`✓ Loaded: ${pilet.name}@${pilet.version}`),
onError: (pilet, error) => console.error(`✗ Failed: ${pilet.name}`, error),
});Configuration Structure
The proxy injects configuration based on the FrontendConfig Go struct:
window.__PRODUCTIFY__ = {
user: {
id: "user-uuid",
email: "user@example.com",
name: "User Name",
username: "username",
},
tenant: {
id: "tenant-id",
name: "Tenant Name",
},
application: {
id: "app-uuid",
},
authType: "oauth",
config: {
// Application configuration from Manager
available_modules: ["example-pilet@1.0.5", "settings-pilet@1.0.5"],
enabled_modules: ["example-pilet", "settings-pilet"],
available_themes: ["light", "dark", "auto"],
enabled_themes: ["light", "dark"],
default_theme: "light",
available_locales: ["en", "es", "fr"],
enabled_locales: ["en", "es"],
default_locale: "en",
key_value_pairs: [{ key: "custom_setting", value: "custom_value" }],
},
};Pilet Loading
Overview
The @productifyfw/core package includes a pilet loader for dynamically loading microfrontend modules (pilets) from a feed service. Pilets are built as ES modules and loaded at runtime.
Loading Pilets
import { loadPilets, type PiletRegistry } from "@productifyfw/core";
const piletRegistry: PiletRegistry = {
extensions: {},
pages: {},
};
await loadPilets("/pilet-feed", piletRegistry, {
onLoad: (pilet) => {
console.log(`✓ Loaded: ${pilet.name}@${pilet.version}`);
},
onError: (pilet, error) => {
console.error(`✗ Failed to load ${pilet.name}:`, error);
},
onComplete: () => {
console.log("All pilets loaded");
},
});Creating a Pilet
Each pilet exports a setup function that receives the PiletApi:
// my-pilet/src/index.ts
import type { PiletApi } from "@productifyfw/core";
import MyWidget from "./components/MyWidget.vue";
export function setup(api: PiletApi) {
// Register a widget extension
api.registerExtension("dashboard-widgets", () => ({
component: MyWidget,
defaults: { title: "My Widget", refreshInterval: 30000 },
}));
// Register a page route
api.registerPage("/my-page", {
component: MyPageComponent,
});
}Building Pilets
Configure Vite to build pilets as ES modules with all dependencies bundled:
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
build: {
lib: {
entry: "./src/index.ts",
name: "MyPilet",
fileName: "index",
formats: ["es"],
},
},
});Build the pilet:
pnpm build
# Output: dist/index.js (ES module with all dependencies bundled)Uploading Pilets
Use the Productify CLI to upload pilets to the Manager:
pfy pilet upload \
--project-id <PROJECT_ID> \
--name my-pilet \
--version 1.0.0 \
--file dist/index.jsConfigure CLI in ~/.pfy/config.yaml:
manager_url: http://manager.localhost # Through the Productify proxy
token: pat_your_token_hereVue 3 Integration Example
import { createApp, reactive, markRaw } from "vue";
import { loadPilets, type PiletRegistry } from "@productifyfw/core";
import App from "./App.vue";
// Expose markRaw for the pilet loader (prevents reactivity overhead)
(window as any).Vue = { markRaw };
// Create reactive registry
const piletRegistry: PiletRegistry = reactive({
extensions: {},
pages: {},
});
const app = createApp(App);
app.provide("piletRegistry", piletRegistry);
app.mount("#app");
// Load pilets after mounting
loadPilets("/pilet-feed", piletRegistry);Rendering Pilet Widgets
<template>
<div class="widgets-grid">
<component
v-for="(widget, index) in dashboardWidgets"
:key="index"
:is="widget.component"
v-bind="widget.defaults || {}"
/>
</div>
</template>
<script setup lang="ts">
import { computed, inject } from "vue";
import type { PiletRegistry } from "@productifyfw/core";
const piletRegistry = inject<PiletRegistry>("piletRegistry");
const dashboardWidgets = computed(
() => piletRegistry.extensions["dashboard-widgets"] || []
);
</script>Language Pack Loading (i18n)
Overview
The @productifyfw/core package includes utilities for loading language packs dynamically from the Manager API. Language packs enable multi-language support with translations fetched at runtime.
Quick Start
import { createI18n } from "@byjohann/vue-i18n";
import { createI18nComposable, loadLanguagePack } from "@productifyfw/core";
// Create i18n instance
const i18n = createI18n({
defaultLocale: "en",
locales: ["en", "hu", "de"],
messages: { en: {}, hu: {}, de: {} },
});
// Load default locale
loadLanguagePack("en").then((messages) => {
i18n.messages.value.en = messages;
});
// Create composable
const useProductifyI18n = createI18nComposable(i18n);
// In component
const { changeLocale, enabledLocales } = useProductifyI18n();
await changeLocale("hu"); // Loads and switches to HungarianAPI Endpoint
Language packs are fetched from:
GET /language-packs/{locale}The proxy automatically injects the application ID and rewrites to:
GET /language-packs/{applicationId}/{locale}Response:
{
"locale": "hu",
"messages": {
"app": {
"title": "Alkalmazásom"
},
"common": {
"save": "Mentés",
"cancel": "Mégse"
}
}
}Language Pack Storage
In the Manager database, translations are stored as key-value pairs with dot notation:
- Database:
{"key": "app.title", "value": "My Application"} - API converts to:
{"app": {"title": "My Application"}}
Example: Language Switcher Component
<script setup lang="ts">
import { ref, watch } from "vue";
import { useI18n } from "@byjohann/vue-i18n";
import { useProductifyI18n } from "../plugins/i18n";
const { locale } = useI18n();
const { changeLocale, enabledLocales } = useProductifyI18n();
const isChanging = ref(false);
const selectedLocale = ref(locale.value);
watch(locale, (newLocale) => {
selectedLocale.value = newLocale;
});
const switchLanguage = async (event: Event) => {
const newLocale = (event.target as HTMLSelectElement).value;
if (newLocale === locale.value) return;
isChanging.value = true;
try {
await changeLocale(newLocale);
} catch (error) {
console.error("Failed to change language:", error);
selectedLocale.value = locale.value;
} finally {
isChanging.value = false;
}
};
</script>
<template>
<select
v-model="selectedLocale"
@change="switchLanguage"
:disabled="isChanging"
>
<option v-for="loc in enabledLocales" :key="loc" :value="loc">
{{ loc.toUpperCase() }}
</option>
</select>
</template>Best Practices
- Initialize Early: Create ProductifyClient instance at app startup
- Use Singleton: Share one instance across your application using dependency injection
- Provide Defaults: Always provide sensible defaults for configuration values
- Cache Results: Cache expensive checks in component state/computed properties
- Handle Missing Config: Gracefully handle cases where config might not be available
- Don't Mutate: Never modify
window.__PRODUCTIFY__directly - Type Safety: Use TypeScript generics for type-safe configuration access
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
- Verify HTML middleware is enabled in Caddyfile
Missing Configuration Values
If specific configuration values are missing:
- Check Manager API
/api/frontend-config/:application_idendpoint - Verify application ID is correct in proxy configuration
- Check Manager has configuration set for the application
- Use default values as fallback
Type Errors
If you're getting TypeScript errors:
- Ensure
@productifyfw/coreis properly installed - Check TypeScript version compatibility (requires 5.0+)
- Verify types are being loaded correctly
- Use explicit type parameters:
getConfig<string>('key')
Advanced Usage
Strict Mode
Enable strict mode to throw errors if configuration is missing:
const pfy = new ProductifyClient({ strict: true });
// Throws error if window.__PRODUCTIFY__ is not availableConfiguration Changes
Listen for configuration changes at runtime:
const pfy = new ProductifyClient({
onChange: (config) => {
console.log("Configuration updated:", config);
},
});
// Later, trigger refresh
pfy.refresh();Singleton Pattern
Use the built-in singleton for global access:
import { getProductifyClient } from "@productifyfw/core";
const pfy = getProductifyClient();
// Returns the same instance every timeType Reference
interface ProductifyFrontendConfig {
user: ProductifyUser;
tenant: ProductifyTenant;
application: ProductifyApplication;
authType?: AuthType;
config?: ProductifyConfig;
}
interface ProductifyUser {
id: string;
email: string;
name: string;
username: string;
}
interface ProductifyTenant {
id: string;
name: string;
}
interface ProductifyApplication {
id: string;
}
interface ProductifyConfig {
[key: string]: unknown;
}