Skip to main content
⚠️ PREVIEW

Power Pages Server Logic:
Server-Side JavaScript for Secure Business Logic

3 code templates: Bulk operations, hidden business logic & external APIs

By Tino Rabe, Microsoft Power Pages MVP • October 31, 2025 • 16 min read

Important: Preview Feature

Server Logic is currently a preview feature in Power Pages. It is not suitable for production environments and is subject to the Supplemental Terms of Use.

Introduction

Microsoft has released Power Pages Server Logic, a new feature that enables server-side JavaScript execution directly in Power Pages. Unlike the client-side Web API, Server Logic code runs on the Power Pages server and is completely hidden from the browser.

❌ Client-Side Web API

Problems:

  • • Code visible in browser (F12)
  • • 100 API calls = 100 HTTP requests
  • • Business logic exposed
  • • No batch support
✅ Server Logic

Solution:

  • • Code on server (hidden)
  • • 1 API call, loop on server
  • • Business logic protected
  • • Pseudo-batch via loops

What is Power Pages Server Logic?

Server Logic enables the execution of ECMAScript 2023 JavaScript on the Power Pages server. The code is stored in Dataverse and invoked via special API endpoints:

POST https://yoursite.powerappsportals.com/_api/serverlogics/YourFunctionName
Content-Type: application/json
__RequestVerificationToken: {{CSRF_TOKEN}}

{ "data": "your payload" }

Key Differences

Aspect Client-Side Web API Server Logic
Execution In browser (JavaScript) On server (ECMAScript 2023)
Code Visibility ❌ Visible via DevTools ✅ Hidden on server
Business Logic ❌ Exposed (F12) ✅ Protected
API Keys ❌ In browser code ✅ Via Server.SiteSetting
Bulk Operations ❌ 100 HTTP requests ✅ 1 request, loop on server
External APIs ❌ CORS issues ✅ Server.Connector.HttpClient

Use Case 1: Bulk Contact Import

Problem: Importing 100+ contacts requires 100 individual API calls from the browser → slow, error-prone, no transactions.

Solution: A single API call to Server Logic, loop runs on the server close to Dataverse.

💼 Bulk Import Template - Client + Server Code

🌐 Client-Side (Browser)

A single API call with all contacts:

// CSV data from upload
const csvData = [
    { firstname: "John", lastname: "Doe", emailaddress1: "john@example.com" },
    { firstname: "Jane", lastname: "Smith", emailaddress1: "jane@example.com" },
    // ... 98 more contacts
];

fetch('/_api/serverlogics/BulkContactImport', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        '__RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
    },
    body: JSON.stringify({ contacts: csvData })
})
.then(response => response.json())
.then(data => {
    if (data.success) {
        console.log(`✅ Import complete!`);
        console.log(`Created: ${data.results.successCount}`);
        console.log(`Failed: ${data.results.failedCount}`);
    }
})
.catch(error => console.error('Error:', error));

🖥️ Server-Side (Power Pages Server Logic)

Loop runs on server, code hidden:

function post() {
    try {
        // Parse Request Body
        const payload = JSON.parse(Server.Context.Body);
        const contacts = payload.contacts;

        // Validation
        if (!contacts || !Array.isArray(contacts)) {
            return JSON.stringify({
                success: false,
                error: "Invalid payload"
            });
        }

        // Initialize results
        const results = {
            successCount: 0,
            failedCount: 0,
            errors: [],
            createdIds: []
        };

        Server.Logger.Log(`Starting bulk import of ${contacts.length} contacts`);

        // SERVER-SIDE LOOP (hidden from browser)
        for (let i = 0; i < contacts.length; i++) {
            const contact = contacts[i];

            try {
                // Dataverse CreateRecord
                const createResponse = Server.Connector.Dataverse.CreateRecord(
                    "contacts",
                    JSON.stringify({
                        firstname: contact.firstname || "",
                        lastname: contact.lastname,
                        emailaddress1: contact.emailaddress1,
                        telephone1: contact.telephone1 || ""
                    })
                );

                const responseData = JSON.parse(createResponse.Body);
                results.createdIds.push(responseData.contactid);
                results.successCount++;

                Server.Logger.Log(`Created contact ${i + 1}/${contacts.length}`);

            } catch (err) {
                results.failedCount++;
                results.errors.push({
                    index: i,
                    contact: contact.lastname,
                    error: err.message
                });
                Server.Logger.Error(`Failed contact ${i}: ${err.message}`);
            }
        }

        Server.Logger.Log(`Bulk import completed. Success: ${results.successCount}, Failed: ${results.failedCount}`);

        return JSON.stringify({
            success: true,
            results: results
        });

    } catch (err) {
        Server.Logger.Error(`Bulk import failed: ${err.message}`);
        return JSON.stringify({
            success: false,
            error: err.message
        });
    }
}

Benefits of This Solution

  • 1 HTTP request instead of 100 (Browser → Server)
  • Server-side loop close to Dataverse = significantly faster
  • Business logic hidden (not visible via F12)
  • Error handling per record with Server.Logger
  • Detailed result object with IDs and errors

Use Case 2: Hidden Business Logic (Commission Calculator)

Problem: Commission calculations in client-side code are visible via DevTools (F12) → competitors can copy algorithms.

Solution: Calculation runs on server, client receives only the final result.

🔒 Hidden Business Logic Template

🌐 Client-Side (Browser)

Simple API call, no business logic visible:

// Client receives only final result
async function getCommission(dealId) {
    const response = await fetch(
        `/_api/serverlogics/CommissionCalculator?dealId=${dealId}`,
        {
            headers: {
                '__RequestVerificationToken': getCSRFToken()
            }
        }
    );

    const result = await response.json();

    // Only final number, no algorithm visible
    return result.commission; // e.g. 12500
}

🖥️ Server-Side (Power Pages Server Logic)

Proprietary algorithm completely hidden:

function get() {
    try {
        // Read query parameter
        const dealId = Server.Context.QueryParameters['dealId'];

        if (!dealId) {
            return JSON.stringify({
                success: false,
                error: "Missing dealId"
            });
        }

        // Get deal data from Dataverse
        const dealResponse = Server.Connector.Dataverse.RetrieveRecord(
            "opportunities",
            dealId,
            "$select=estimatedvalue,customerid,statecode"
        );

        const deal = JSON.parse(dealResponse.Body);

        // Get customer data (for tier check)
        const customerResponse = Server.Connector.Dataverse.RetrieveRecord(
            "accounts",
            deal._customerid_value,
            "$select=accountid,customertypecode,address1_country"
        );

        const customer = JSON.parse(customerResponse.Body);

        // PROPRIETARY ALGORITHM (hidden from browser)
        let commission = calculateCommissionInternal(
            deal.estimatedvalue,
            customer.customertypecode,
            customer.address1_country
        );

        Server.Logger.Log(`Commission calculated for deal ${dealId}: ${commission}`);

        return JSON.stringify({
            success: true,
            commission: commission,
            dealId: dealId
        });

    } catch (err) {
        Server.Logger.Error(`Commission calculation failed: ${err.message}`);
        return JSON.stringify({
            success: false,
            error: "Calculation failed"
        });
    }
}

// INTERNAL FUNCTION (not callable from browser)
function calculateCommissionInternal(value, tierCode, country) {
    let rate = 0.10; // Base: 10%

    // Tier-based adjustment (proprietary)
    if (tierCode === 3) rate = 0.15;      // Platinum: 15%
    else if (tierCode === 2) rate = 0.12; // Gold: 12%

    // Region multiplier (proprietary)
    if (country === "DE" || country === "AT" || country === "CH") {
        rate *= 1.2; // DACH: +20%
    }

    // Value-based bonus (proprietary)
    if (value > 100000) {
        rate += 0.05; // +5% for >100k
    }

    return Math.round(value * rate);
}

Benefits of This Solution

  • Proprietary algorithm completely hidden
  • Client receives only final result (no reverse engineering possible)
  • Competitors cannot copy formula
  • Algorithm updates without client deployment
  • Additional server-side validation possible

Use Case 3: External API Integration

Problem: External REST APIs (payment providers, SMS services, weather APIs, etc.) require API keys. In client-side code, these would be visible via F12 → security risk.

Solution: Store API keys in site settings, calls via Server Logic → keys never in browser.

🔗 External API Template (Generic REST API)

🌐 Client-Side (Browser)

No API keys in browser, generic API call:

// Client doesn't know API keys
async function callExternalService(actionType, payload) {
    const response = await fetch(
        '/_api/serverlogics/ExternalAPIProxy',
        {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                '__RequestVerificationToken': getCSRFToken()
            },
            body: JSON.stringify({
                action: actionType,
                data: payload
            })
        }
    );

    const result = await response.json();

    if (result.success) {
        console.log("API call successful:", result.data);
        return result.data;
    } else {
        console.error("API call failed:", result.error);
        throw new Error(result.error);
    }
}

🖥️ Server-Side (Power Pages Server Logic)

API keys securely from site settings, generic REST API call:

function post() {
    try {
        const payload = JSON.parse(Server.Context.Body);
        const action = payload.action;

        // API keys from encrypted site settings (NEVER in browser)
        const apiKey = Server.SiteSetting.Get("ExternalAPI_ApiKey");
        const apiBaseUrl = Server.SiteSetting.Get("ExternalAPI_BaseUrl");

        if (!apiKey || !apiBaseUrl) {
            throw new Error("API credentials not configured");
        }

        Server.Logger.Log(`External API call: ${action}`);

        // Example: Payment API, SMS service, weather API, etc.
        let apiEndpoint = "";
        let requestBody = {};

        // Action-based routing
        if (action === "sendSMS") {
            apiEndpoint = `${apiBaseUrl}/messages`;
            requestBody = {
                to: payload.data.phoneNumber,
                text: payload.data.message,
                from: "YourServiceName"
            };
        } else if (action === "verifyPayment") {
            apiEndpoint = `${apiBaseUrl}/payments/verify`;
            requestBody = {
                transactionId: payload.data.transactionId,
                amount: payload.data.amount
            };
        } else if (action === "getWeather") {
            apiEndpoint = `${apiBaseUrl}/weather?city=${payload.data.city}`;
            // GET request without body
        } else {
            throw new Error(`Unknown action: ${action}`);
        }

        // EXTERNAL API CALL (via Server.Connector.HttpClient)
        let response;

        if (action === "getWeather") {
            // GET request
            response = Server.Connector.HttpClient.GetAsync(
                apiEndpoint,
                JSON.stringify({
                    "Authorization": `Bearer ${apiKey}`,
                    "Accept": "application/json"
                })
            );
        } else {
            // POST request
            response = Server.Connector.HttpClient.PostAsync(
                apiEndpoint,
                JSON.stringify(requestBody),
                JSON.stringify({
                    "Authorization": `Bearer ${apiKey}`,
                    "Content-Type": "application/json"
                }),
                "application/json"
            );
        }

        if (!response.IsSuccessStatusCode) {
            throw new Error(`API error: ${response.ReasonPhrase}`);
        }

        const apiResult = JSON.parse(response.Body);

        Server.Logger.Log(`External API success: ${action}`);

        return JSON.stringify({
            success: true,
            data: apiResult,
            action: action
        });

    } catch (err) {
        Server.Logger.Error(`External API failed: ${err.message}`);
        return JSON.stringify({
            success: false,
            error: err.message
        });
    }
}

⚙️ Required Site Settings

ServerLogic/AllowedDomains = api.yourservice.com,api.alternative.com
ExternalAPI_ApiKey = [Your API Key]
ExternalAPI_BaseUrl = https://api.yourservice.com/v1

Examples for External APIs: Stripe/PayPal (payment), Twilio/MessageBird (SMS), OpenWeather/WeatherAPI, SendGrid/Mailgun (email), Google Maps/HERE (geocoding)

Benefits of This Solution

  • API keys never in browser (via Server.SiteSetting)
  • No CORS issues (server → API directly)
  • Synchronous processing (no Power Automate delay)
  • Error handling with Server.Logger
  • Centralized credential management

Server Objects API Reference

Server Logic provides several built-in objects:

Server.Connector.Dataverse

CRUD operations on Dataverse tables

// Create
Server.Connector.Dataverse.CreateRecord(
    "contacts",
    JSON.stringify({...})
);

// Read
Server.Connector.Dataverse.RetrieveRecord(
    "contacts",
    contactId,
    "$select=firstname,lastname"
);

// Update
Server.Connector.Dataverse.UpdateRecord(
    "contacts",
    contactId,
    JSON.stringify({...})
);

// Delete
Server.Connector.Dataverse.DeleteRecord(
    "contacts",
    contactId
);

Server.Connector.HttpClient

HTTP requests to external APIs

// GET
Server.Connector.HttpClient.GetAsync(
    url,
    headers
);

// POST
Server.Connector.HttpClient.PostAsync(
    url,
    body,
    headers,
    contentType
);

// PUT
Server.Connector.HttpClient.PutAsync(
    url,
    body,
    headers,
    contentType
);

// DELETE
Server.Connector.HttpClient.DeleteAsync(
    url,
    headers
);

Server.Context

Request metadata & user info

// Request body
const payload = JSON.parse(
    Server.Context.Body
);

// Query parameters
const id = Server.Context.QueryParameters['id'];

// HTTP method
const method = Server.Context.HttpMethod;

// Request headers
const auth = Server.Context.Headers['Authorization'];

Server.Logger

Diagnostic logging (DevTools extension)

// Info
Server.Logger.Log("Info message");

// Warning
Server.Logger.Warn("Warning message");

// Error
Server.Logger.Error("Error message");

// Logs visible in DevTools extension

Server.SiteSetting

Read encrypted site settings

// Store API keys securely
const apiKey = Server.SiteSetting.Get(
    "ExternalAPI_ApiKey"
);

const timeout = Server.SiteSetting.Get(
    "ServerLogic/TimeoutInSeconds"
);

Server.User

Current user (authenticated)

// User info
if (Server.User) {
    const fullname = Server.User.fullname;
    const email = Server.User.emailaddress1;
    const userId = Server.User.contactid;
} else {
    // Anonymous user
}

Setup Guide

Prerequisites

  • 1.
    Enhanced Data Model
    Server Logic only works with Enhanced Data Model. Migration required if using Standard Model.
  • 2.
    Power Pages Studio
    Access to "Setup" workspace → "Server Logic (Preview)"
  • 3.
    VS Code with Power Platform Tools
    For code authoring with IntelliSense

Step-by-Step Setup

Step 1: Create Server Logic

  1. Open Power Pages Studio
  2. Setup workspace → Server Logic (Preview)
  3. Click "New Server Logic"
  4. Enter name (e.g. "BulkContactImport")
  5. Assign web role

Step 2: Write Code

  1. Click "Edit Code"
  2. Select "Open Visual Studio Code"
  3. Insert code template (function get/post/put/del)
  4. Save (Ctrl+S)

Step 3: Table Permissions

  1. Security workspace → Table Permissions
  2. Create new permission for affected tables
  3. Set privileges (Create, Read, Update, Delete)
  4. Assign web role

Step 4: Site Settings (optional)

ServerLogic/Enabled = true
ServerLogic/TimeoutInSeconds = 120
ServerLogic/AllowedDomains = api.example.com
ServerLogic/MaxMemoryUsageInBytes = 10485760

Limitations & Restrictions

Preview Status

  • Not production-ready: Preview features are subject to change, breaking changes possible
  • No SLA: No service level agreements, no production support
  • Enhanced Data Model required: Only works with EDM, not with Standard Model

Technical Limitations

❌ Not Available

  • • Browser APIs (fetch, XMLHttpRequest)
  • • Node.js APIs (Buffer, fs, process)
  • • DOM manipulation
  • • window, document objects
  • • npm packages

✅ Available

  • • ECMAScript 2023 standard
  • • Server.Connector.* APIs
  • • Server.Context, Server.User
  • • Server.Logger for diagnostics
  • • JSON, Array, String methods

⏱️ Performance Limits

  • Timeout: 120 seconds (default), max 240 seconds
  • Memory: 10 MB per function
  • Content-Types: application/json, text/html, application/x-www-form-urlencoded
  • Allowed domains: Must be explicitly defined in site settings

Questions about Power Pages?

In a free 30-minute consultation, we'll analyze your requirements and show you the possibilities of the Power Platform for your project.

Book Consultation Now
Tino Rabe

Tino Rabe

Microsoft Power Pages MVP

I help mid-sized companies build secure and GDPR-compliant customer portals with Microsoft Power Pages. My focus: Fast implementation, measurable ROI, no vendor lock-ins.