Skip to content

Frontend Integration Guide

Complete guide for integrating Productify into your frontend applications.

Table of Contents

Overview

The Productify frontend integration consists of two main parts:

  1. Configuration Injection: The proxy injects configuration data into window.__PRODUCTIFY__
  2. Client Library: TypeScript library (@productifyfw/core) for type-safe access. Framework bindings (@productifyfw/vue, @productifyfw/react) are planned but do not exist — see Known Issues

Installation

Configure npm to resolve @productifyfw packages from GitHub Packages by adding to .npmrc:

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

Then install:

bash
npm install @productifyfw/core

Or with other package managers:

bash
yarn add @productifyfw/core
pnpm add @productifyfw/core

There are no framework-binding packages to install: @productifyfw/vue and @productifyfw/react do not exist yet (Known Issues). Use @productifyfw/core directly.

Configuration 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__:

html
<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"
    },
    "config": {
      "enabled_modules": ["example-pilet@1.0.5"],
      "default_theme": "light",
      "default_locale": "en",
      "key_value_pairs": [{ "key": "maxItemsPerPage", "value": "25" }]
    }
  }
</script>
<script>
  window.__PRODUCTIFY__ = JSON.parse(
    document.getElementById("productify-config").textContent
  );
</script>

This injection happens before your application JavaScript loads, ensuring the configuration is available immediately.

Configuration Structure

typescript
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)
  authType?: 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;
    key_value_pairs?: Array<{ key: string; value: string }>;
    [key: string]: unknown;
  };
}

Library Usage

Basic Setup

typescript
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

typescript
// 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();

// Configuration values
const maxItems = pfy.getConfig<number>("maxItemsPerPage", {
  defaultValue: 10,
});
const featureEnabled = pfy.isConfigEnabled("newCheckout");

// Nested values with dot notation
const mode = pfy.getNestedConfig<string>("ui.theme.mode");

Themes and Locales

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

typescript
// 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 (planned)

Not yet implemented

@productifyfw/vue does not exist — the example below will not resolve. Use @productifyfw/core directly (createI18nComposable is exported from core). See Known Issues.

The intended design: a @productifyfw/vue package providing a plugin and a useProductify() composable over the core client.

Plugin Setup

typescript
// main.ts
import { createApp } from "vue";
import { ProductifyPlugin } from "@productifyfw/vue";
import App from "./App.vue";

const app = createApp(App);
app.use(ProductifyPlugin);
app.mount("#app");

Composition API Usage

vue
<script setup lang="ts">
import { computed } from "vue";
import { useProductify } from "@productifyfw/vue";

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 (planned)

Not yet implemented

@productifyfw/react does not exist — the example below will not resolve. Use @productifyfw/core directly. See Known Issues.

The intended design: a @productifyfw/react package providing a context provider and hooks over the core client.

App Setup

typescript
// App.tsx
import { ProductifyProvider } from "@productifyfw/react";
import Dashboard from "./Dashboard";

function App() {
  return (
    <ProductifyProvider>
      <Dashboard />
    </ProductifyProvider>
  );
}

export default App;

Component Usage

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

typescript
window.__PRODUCTIFY__.config.enabled_modules = [
  "navigation@1.2.3",
  "checkout@2.0.1",
  "analytics@0.5.0",
];

Loading Pilets

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

See the frontend integration library guide for details on the pilet API and integrity verification.

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

Important: Never rely solely on client-side checks for security. Always validate permissions on the backend.

typescript
// [OK] Good: Hide UI elements
if (!pfy.isConfigEnabled("adminUi")) {
  // Don't show admin button
}

// [NO] Bad: Only client-side protection
if (pfy.isConfigEnabled("adminUi")) {
  // 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 innerHTML with configuration values
  • Sanitize any user-controlled content before rendering
  • Use framework's built-in XSS protection (Vue templates, React JSX, etc.)

Migration Guide

Migrating to the @productifyfw/core Library

If you were previously accessing window.__PRODUCTIFY__ directly, migrate to the type-safe client library:

typescript
// Old
const user = window.__PRODUCTIFY__?.user;
const config = window.__PRODUCTIFY__?.config;

// New
import { getProductifyClient } from "@productifyfw/core";
const pfy = getProductifyClient();
const user = pfy.getUser();
const config = pfy.getConfigObject();

Benefits of Migration

  • Type Safety: Full TypeScript support with intellisense
  • Null Safety: Proper handling of missing configuration
  • Consistent API: Same API across different frameworks
  • Future-Proof: Easy to extend with new features

Best Practices

  1. Initialize Early: Create ProductifyClient instance at app startup
  2. Use Singleton: Share one instance across your application
  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

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

Type Errors

Ensure TypeScript configuration includes:

json
{
  "compilerOptions": {
    "lib": ["ES2020", "DOM"],
    "moduleResolution": "bundler"
  }
}

Stale Configuration

If configuration doesn't update:

typescript
// Manually refresh configuration; onChange fires when content changed
const pfy = new ProductifyClient({
  onChange: (config) => console.log("Configuration updated:", config),
});

pfy.refresh();

Support

For issues, questions, or contributions: