Skip to content

Frontend Integration - @productifyfw/core

Complete guide for integrating Productify configuration access and pilet loading in your applications.

Table of Contents

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:

  1. User/Tenant Data: Extracted from HTTP headers set by authentication middleware:

    • X-User-ID, X-User-Email, X-User-Name, X-User-Username
    • X-Tenant-ID, X-Tenant-Name
    • X-Auth-Type
  2. Application Config: Fetched from Manager API endpoint /api/frontend-config/:application_id

  3. Injection: Combined into window.__PRODUCTIFY__ object before your application loads

  4. Pilet 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:

ini
@productifyfw:registry=https://npm.pkg.github.com

Then install:

bash
npm install @productifyfw/core
# or
pnpm add @productifyfw/core
# or
yarn add @productifyfw/core

No 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

typescript
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

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

typescript
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

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

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

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

bash
pnpm build
# Output: dist/index.js (ES module with all dependencies bundled)

Uploading Pilets

Use the Productify CLI to upload pilets to the Manager:

bash
pfy pilet upload \
  --project-id <PROJECT_ID> \
  --name my-pilet \
  --version 1.0.0 \
  --file dist/index.js

Configure CLI in ~/.pfy/config.yaml:

yaml
manager_url: http://manager.localhost # Through the Productify proxy
token: pat_your_token_here

Vue 3 Integration Example

typescript
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

vue
<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

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

API 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:

json
{
  "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

vue
<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

  1. Initialize Early: Create ProductifyClient instance at app startup
  2. Use Singleton: Share one instance across your application using dependency injection
  3. Provide Defaults: Always provide sensible defaults for configuration values
  4. Cache Results: Cache expensive checks in component state/computed properties
  5. Handle Missing Config: Gracefully handle cases where config might not be available
  6. Don't Mutate: Never modify window.__PRODUCTIFY__ directly
  7. Type Safety: Use TypeScript generics for type-safe configuration access

Troubleshooting

Configuration Not Found

If window.__PRODUCTIFY__ is undefined:

  1. Verify application is accessed through Productify proxy
  2. Check proxy configuration and middleware
  3. Ensure correct domain/URL is used
  4. Check browser console for injection errors
  5. Verify HTML middleware is enabled in Caddyfile

Missing Configuration Values

If specific configuration values are missing:

  1. Check Manager API /api/frontend-config/:application_id endpoint
  2. Verify application ID is correct in proxy configuration
  3. Check Manager has configuration set for the application
  4. Use default values as fallback

Type Errors

If you're getting TypeScript errors:

  1. Ensure @productifyfw/core is properly installed
  2. Check TypeScript version compatibility (requires 5.0+)
  3. Verify types are being loaded correctly
  4. Use explicit type parameters: getConfig<string>('key')

Advanced Usage

Strict Mode

Enable strict mode to throw errors if configuration is missing:

typescript
const pfy = new ProductifyClient({ strict: true });
// Throws error if window.__PRODUCTIFY__ is not available

Configuration Changes

Listen for configuration changes at runtime:

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

typescript
import { getProductifyClient } from "@productifyfw/core";

const pfy = getProductifyClient();
// Returns the same instance every time

Type Reference

typescript
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;
}

See Also