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.
Problems:
- • Code visible in browser (F12)
- • 100 API calls = 100 HTTP requests
- • Business logic exposed
- • No batch support
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
- Open Power Pages Studio
- Setup workspace → Server Logic (Preview)
- Click "New Server Logic"
- Enter name (e.g. "BulkContactImport")
- Assign web role
Step 2: Write Code
- Click "Edit Code"
- Select "Open Visual Studio Code"
- Insert code template (function get/post/put/del)
- Save (Ctrl+S)
Step 3: Table Permissions
- Security workspace → Table Permissions
- Create new permission for affected tables
- Set privileges (Create, Read, Update, Delete)
- 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