PREVIEW 31 October 2025 16 min read

Power Pages Server Logic: Server-Side JavaScript for Secure Business Logic (Preview)

Power Pages Server Logic (Preview): Server-side JavaScript, Dataverse connector, HttpClient and external APIs. Includes 3 ready-to-use code templates.

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.

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 vs. Server Logic

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:

Server Logic — Endpoint
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 Via 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.

Client-Side (Browser)

A single API call with all contacts:

Client — Bulk Import Request
// CSV data from upload
const csvData = [
    { firstname: "John", lastname: "Doe", emailaddress1: "[email protected]" },
    { firstname: "Jane", lastname: "Smith", emailaddress1: "[email protected]" },
    // ... 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:

Server — BulkContactImport
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.

Client-Side (Browser)

Simple API call, no business logic visible:

Client — Get Commission
// 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:

Server — CommissionCalculator
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 the 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 — a security risk.

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

Client-Side (Browser)

No API keys in browser, generic API call:

Client — External API Proxy
// Client doesn't know the 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:

Server — ExternalAPIProxy
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 is required if you are using the Standard Model.
  • 2.
    Power Pages Studio
    Access to the "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 a name (e.g. "BulkContactImport")
  5. Assign a web role

Step 2: Write Code

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

Step 3: Table Permissions

  1. Security workspace → Table Permissions
  2. Create a new permission for the affected tables
  3. Set privileges (Create, Read, Update, Delete)
  4. Assign a 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 are possible
  • ×
    No SLA: No service level agreements, no production support
  • ×
    Enhanced Data Model required: Only works with EDM, not with the 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

Want to Explore Server Logic?

Get expert guidance on implementing Server Logic for your Power Pages project.

Book Consultation
Tino Rabe

Tino Rabe

Power Pages Spezialist · Former Microsoft MVP

Power Pages specialist, former Microsoft MVP. I help companies build secure customer portals: architecture workshop, weekly coaching, security audits.

When was your portal last independently reviewed?

Fixed-fee security audit, or just talk it through first.

Book a call