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
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 | 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:
// 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:
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 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 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 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:
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
- Open Power Pages Studio
- Setup workspace → Server Logic (Preview)
- Click "New Server Logic"
- Enter a name (e.g. "BulkContactImport")
- Assign a web role
Step 2: Write Code
- Click "Edit Code"
- Select "Open Visual Studio Code"
- Insert the code template (function get/post/put/del)
- Save (Ctrl+S)
Step 3: Table Permissions
- Security workspace → Table Permissions
- Create a new permission for the affected tables
- Set privileges (Create, Read, Update, Delete)
- 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