ASP.js for Visual Studio Code

mobileFX  |  Architecture  |  Diagram

ASP.js brings Classic ASP back to life over Node.js … with some modern and innovative twists!

ASP.js is not confined to the mere mimesis of Classic ASP for reasons of legacy; it ascends beyond imitation. It reveals itself as a layered ecosystem, grounded in the archē of a database-first philosophy — where data is the primordial substance from which all logic flows. Upon this foundation rises a resilient full-stack application architecture, harmonized with cross-tier, end-to-end instruments of debugging, monitoring and security. In this way, the framework empowers the developer with the clarity, speed, and discipline required for the creation of data-driven business applications, uniting continuity with innovation into a single, coherent whole.

extension demo

Layer Frameworks Level
🟩 Web Layer
 
IISX / SITEX / HTTPX / ASPX
Low-level web server runtime, emulating Classic ASP on Node.js.
Low
 
🟨 Data Access Layer
 
SQLX
Native dataset engine with SHAPE queries, pivots, and master–detail semantics.
Mid-Low
 
🟧 ORM Business Layer
 
ORMX
Modern ORM framework with field to property wrapping, and dynamic detail binding.
Mid-High
 
🟦 Application Layer
 
Backstage
Metadata-driven web framework that feels native, powered by Emscripten datasets.
High
 

At every layer you can choose how close to the metal — or how high-level — you want to work. And across them all, ASP.js delivers a robust end-to-end multi-tier Debugger. Step seamlessly from browser JavaScript → server logic → back to client. No other framework can do this. Unlike Angular, React, or Vue, which bury you in boilerplate, REST glue, and fractured tooling, ASP.js gives you one coherent stack where security, datasets, and UI are built-in.

Regardless of whether it fits your present needs or becomes part of your stack, take a moment to recognize the endeavor: an earnest effort to carry something old into the new, preserving its essence while opening paths to fresh creation.

Key Features

  1. Classic ASP Language Support

    • Rich syntax highlighting for .asp and .inc files.
    • Accurate parsing of <% … %> script blocks and embedded JavaScript.
    • Diagnostics, code validation, and auto-completion for ASP symbols.
    • Ctrl+Click navigation for includes and server-side references.
    • Integrated ASP markup formatter and hybrid JS grammar support.
  2. ASP Runtime Emulation

    • Full emulation of Application, Session, Request, Response, and Server objects.
    • ASP pages transpiled into pure JavaScript with precise source mapping.
    • Execution on Node.js for cross-platform hosting.
    • Built-in CSRF protection, secure cookies, and per-site security policies.
    • Seamless integration with the Backstage framework for data binding and ORM support.
  3. Integrated Web Server & Site Manager

    • Lightweight IIS replacement (IISX.js) hosting multiple sites on one root domain.
    • Automatic HTTP→HTTPS redirection, SAN SSL certificates, and host header–based routing.
    • Per-site configuration: MIME types, method allowlists, folder & extension rules.
    • Strong security features: CORS, CSP, anti-flood controls, AutoBan (BANX.js), and rate limiting (RATEX.js).
  4. State-of-the-Art Debugging

    • Unified server-side (Node.js) and client-side (Chrome) debugging.
    • Step seamlessly between ASP server code and client JavaScript.
    • Breakpoints, variable inspection, and call stacks in both contexts.
    • Accurate source mapping: compiled JavaScript lines are traced back to original ASP.
    • Variable inspection works across Proxies, Collections, and ASP objects.
  5. Customizable Debugger Settings

    • Flexible configuration of Node.js and Chrome debug ports.
    • Adjustable workspace and web root mapping for complex project structures.
    • Supports multiple site roots and domain-level project generation.
  6. Project & Certificate Management

    • Quick command to scaffold a new ASP project with sample pages and SSL.
    • Built-in CA Root generation and self-signed certificate management.
    • Auto-generates launch.json and project layout for immediate debugging.
    • Streamlines developer onboarding with ready-to-run sites and breakpoints.

Integration with VS Code

mobileFX  |  Architecture  |  Diagram

ASPjs Architecture

The extension enhances Visual Studio Code by:

Requirements

Create New ASP Project in seconds

You can create a production-level ASP host in seconds! From the Command Palette select New ASP Project command and provide the top-level domain name of your project. The wizard will generate the entire project structure, including a mock CA root authority and self-signed SSL certificates for your tests. During project initialization the extension will copy ASP.js Runtime package, and a free version of SQLX, the RDBMS Runtime package for APS.js that offers read-only database access. When the project is created, simply select Debug ASP an start debugging!

extension demo


⚡ASP.js Developer’s Guide

mobileFX  |  @mobilefx/aspx

Table of Contents

1. Part I – Framework Overview

Contents

1.1 Preface

The ASP Emulation Framework is a Node.js-based platform that recreates and extends the classic ASP programming model, while embedding modern web standards, security practices, and developer productivity features. This guide is written for developers, system architects, and researchers who seek both theoretical grounding and practical know-how. It emphasizes scientific validity, presenting each module with its objectives and innovations, and demonstrates how the framework unifies hosting, runtime, ORM, and MMV layers into a cohesive whole.

1.2 Framework Vision

aspjs-eco-system

1.3 System Architecture Overview

The framework is designed in tiered architecture, with clear separation of concerns:

  1. Hosting & Site Management

    • IISX.js and HTTPX.js provide multi-site hosting, SSL, request routing, and ASP runtime integration.
  2. Security & Compliance

    • BANX.js and RATEX.js enforce IP banning, rate limiting, and automated security rules at runtime.
  3. ASP Runtime

    • ASPX.js emulates ASP semantics, while ERRX.js extends error handling into context-aware exceptions.
  4. Data Access & ORM

    • SQLX.js maps database types, and ORMX.js provides an ORM with hierarchical datasets, foreign key resolution, and AI descriptors.
  5. Utilities

    • UTILX.js supplies secure input sanitization, anti code-injection guards, templating, and string manipulation.
  6. Backstage Framework

    • A full-featured data binding framework connecting client-side Emscripten datasets with server-side ORM models.
  7. Monitoring & Analytics

    • Unified exception logging, session analytics, and compliance reporting.
  8. Developer Experience

    • Integrated seamless client–server debugging, extensibility points, and deployment automation.
    • The debugger is state of the art: it allows developers to step seamlessly between client-side and server-side execution contexts.
    • ASP pages are compiled into pure JavaScript with mapping tables that correlate compiled lines back to source ASP lines, enabling simple stepping between ASP and JavaScript code.
    • Both plain JavaScript modules and ASP pages are supported, with the ability to switch smoothly from the browser’s client debugger to the Node.js server debugger (and vice versa).

At each tier, the framework introduces innovations that go beyond legacy ASP or conventional Node.js stacks, resulting in a platform that is simultaneously familiar, secure, and future-ready.

2. Part II – Hosting & Site Management

extension demo

Contents

2.1 IISX.js – SiteManager

Objectives

HTTP Proxy & Routing Model

TLS & Certificate Handling

Early-Listener Security (pre-application)

Operational Resilience & DX

💡 Why It Matters
IISX.js centralizes TLS, routing, and edge security for every hosted site. By performing host/ban checks and flood detection at the listener, it reduces load on application layers and provides a single place to enforce organizational security posture.


2.2 HTTPX.js – WebServer

Objectives

Innovations

💡Why It Matters > HTTPX.js transforms a plain Node.js HTTP server into a full ASP-style web server with enterprise-grade security and modern web capabilities (CORS, CSP, streaming).
It is the execution layer that turns per-site definitions (SITEX.js) into real runtime behavior, ensuring every request is both safe and efficient.


2.3 SITEX.js – Site & Proxy

Objectives

Innovations

💡Why It Matters Where IISX.js is the multi-tenant orchestrator and HTTPX.js is the execution engine, SITEX.js is the policy brain. It centralizes all per-site rules so that developers can configure powerful security, CORS, CSP, encryption, and access controls without re-writing server logic.


3. Part III – Security & Compliance

Contents

3.1 BANX.js – AutoBan Manager

aspjs-eco-system

Objectives

Innovations

💡 Why It Matters
BANX.js acts as the first line of defense against hostile traffic. By separating strategy definition, violation tracking, and ban enforcement, it achieves a balance of flexibility and robustness. Developers gain a security layer that adapts to both high-severity attacks (immediate bans) and low-and-slow abuses (rate-limited bans), while preserving forensic data for compliance and audits.


3.2 RATEX.js – Rate Limiter

Objectives

Innovations

💡 Why It Matters
RATEX.js is the first pressure valve in the stack: cheap, accurate throttling that shields application code from surges while feeding precise signals to BANX.js. This two-stage design (throttle → escalate) preserves good traffic, absorbs bursts gracefully, and clamps down on abuse only when warranted.


3.3 Security Innovations

Objectives

Innovations (Defense-in-Depth)

💡 Why It Matters
Security isn’t a bolt-on: it is embedded at every layer—from the listener to the data model. Early rejection of bad traffic preserves capacity; per-site policies reduce blast radius; rate-limit + autoban provide proportionate responses; and uniform logging produces audit-grade evidence. The result is a platform that is secure by default, observable, and adaptable to evolving threats.


4. Part IV – ASP Runtime

Contents

4.1 ASPX.js – ASP Emulation

The ASPX.js module is the heart of the framework: it emulates the classic ASP runtime model on top of Node.js.
It provides all the familiar ASP intrinsic objects (Request, Response, Session, Application, Server) while introducing a modern execution and debugging context.

By combining JavaScript’s async model with ASP semantics, ASPX.js allows developers to run ASP pages compiled into pure JavaScript, while still coding against the objects and idioms of Classic ASP. This duality is further enhanced with a debugger integration that can seamlessly map between the compiled JavaScript lines and the original ASP source.


4.1.1 ASPRequest

Objectives

Object Model & Lifecycle

Security & Enforcement

Innovations Beyond Classic ASP

Differences vs Classic ASP

💡 Why It Matters
ASPRequest bridges raw Node.js HTTP with the emulated ASP world. It ensures that every input — from query strings to uploads — is normalized, validated, and secured before reaching page logic.
Developers get the familiar ergonomics of Classic ASP, but with modern enhancements like hash parsing, proactive upload filtering, and direct security hooks, making request handling both safer and more expressive.


4.1.2 ASPResponse

Objectives

Object Model & Lifecycle

Security & Enforcement

Innovations Beyond Classic ASP

Differences vs Classic ASP

💡 Why It Matters
ASPResponse transforms output from a simple write buffer into a security-enforcing boundary. Developers can use familiar ASP-style methods, but every response is automatically wrapped in modern protections — secure cookies, CSRF defense, strict headers. This duality makes the response both ergonomic for legacy ASP developers and robust for contemporary security requirements.


4.1.3 ASPSession

Objectives

Object Model & Lifecycle

Surface & Data Structures

Security & Cryptography

Operational Diagnostics & Jobs

Integration Points

Innovations Beyond Classic ASP

Differences vs Classic ASP

💡 Why It Matters
ASPSession is more than a key–value bag: it’s a security-aware, observable, and operational nucleus for each user. Ephemeral per-session crypto, CSRF coupling, JIT mode, and built-in job tracking let apps coordinate long tasks and protect state without custom scaffolding. The result is lower ceremony, stronger guarantees, and clearer insight into session behavior at scale.


4.1.4 ASPApplication

Objectives

Object Model & Lifecycle

Security & Isolation

Innovations Beyond Classic ASP

Differences vs Classic ASP

💡 Why It Matters
ASPApplication provides a safe, per-site global memory space that Classic ASP developers expect, but redesigned for a multi-tenant, async Node.js world. It enables caching, counters, and configuration sh


4.1.5 ASPServer

Objectives

Object Model & Surface

Design & Safety Properties

Innovations Beyond Classic ASP

Differences vs Classic ASP

💡 Why It Matters
ASPServer is the developer’s toolbelt inside ASP pages—now redesigned for security and portability. By constraining object creation, hardening path resolution, and offering content-addressed script persistence, it turns common tasks into safe, deterministic operations. The result is a familiar surface for Classic ASP developers that maps cleanly to modern Node.js practices without inheriting legacy risks.


4.1.6 ASPRuntime

aspjs-eco-system

Objectives

Architecture at a Glance

Request Lifecycle (high level)

  1. Admission: HTTPX.js passes a normalized request to ASPRuntime.
  2. Context: ASPRuntime creates ASPContext and attaches intrinsics (Request/Response/Session/Application/Server).
  3. Security Preflight: validate CSRF (if required), check site policy switches (CSP/CORS already applied by HTTPX.js).
  4. Compilation/Binding: resolve the target page, compile ASP → JS, cache artifacts, bind mapping tables.
  5. Execution: call page entry with ASPContext (sync/async allowed).
  6. Commit & Cleanup: flush buffers, finalize cookies/headers, fire teardown hooks, process JIT sessions, release references.

Compilation & Caching Pipeline

Debugger & Mapping Integration

Concurrency & Determinism

Security Responsibilities

Error Handling & Observability

Performance Characteristics

Compatibility Notes vs Classic ASP

Innovations Beyond Classic ASP

💡 Why It Matters
ASPRuntime is the brain of the framework: it preserves Classic ASP’s developer ergonomics while delivering modern guarantees—async safety, strong security defaults, and source-accurate debugging. By centralizing compilation, policy, and lifecycle, it makes page execution predictable, observable, and fast, turning legacy semantics into a production-grade Node.js runtime. The result is a platform where classic patterns and contemporary engineering co-exist without compromise.


4.1.7 ASPContext

Objectives

Object Model & Lifecycle

Responsibilities

Innovations Beyond Classic ASP

Differences vs Classic ASP

💡 Why It Matters
ASPContext is the spinal cord of ASP.js: it carries every intrinsic, tracks per-request state, and enables source-accurate debugging.
By combining classic semantics with proxy-based scope control and async awareness, it ensures each request is isolated, debuggable, and secure, forming the foundation on which the ASP emulation rests.


4.2 ERRX.js – Exception Handling

Objectives

Exception Model

Integration Points

Innovations Beyond Classic ASP

Differences vs Classic ASP

💡 Why It Matters
ERRX.js turns error handling into a predictable, structured, and observable part of the framework. Developers gain richer diagnostics than Classic ASP ever provided, with direct source mapping, contextual logging, and full lifecycle integration. The result: faster debugging in development and stronger audit/compliance guarantees in production.


5. Part V – Data Access & ORM

Contents

5.1 SQLX.js – Database Abstraction

Objectives

Architecture & Drivers

Core Types (from the native API)

SQLX Native Node.js Dataset Class:

Convenience Layer

Editing & Commit Semantics

Interoperability with ORMX.js

Limitations & Current Scope

💡 Why It Matters
SQLX.js delivers an ADO-like developer experience—hierarchical, cursor-based datasets with rich metadata—on a modern Node.js stack. It pairs UI-aware fields, lookup resolution, constraints, and auditing with a fast native engine, letting you build data-heavy screens and workflows without hand-rolling boilerplate. By separating the dataset engine (SQLX) from the entity layer (ORMX), the framework keeps performance predictable while giving you a clean path to higher-level modeling.


5.2 Native Data Engine Deep Dive

5.2.1 Overview

The native SQLX engine implements an ADO-inspired Dataset with rich DataField metadata and a high-performance SQLite driver. It provides cursor navigation, in-place editing, master–detail hierarchies, UI/validation hints on columns, metadata-driven lookups, a pivot virtual table with SQL macros, and zero-ceremony auditing.

5.2.2 Dataset — Design Contracts & Lifecycle

Cursor & editing surface (selected)

Hierarchies

5.2.3 DataField — Type System & UI/Lookup Metadata

Each column is a DataField with database facts + UI semantics:

5.2.4 SQLite Driver — Runtime Behaviors

5.2.5 Lookups — Caching, Filtering, Reverse Mapping

5.2.6 Pivot Engine — Virtual Table + SQL Macros

5.2.7 Audit — Table, Triggers, Attribution

5.2.8 Performance Characteristics

5.2.9 Current Scope & Limitations

💡 Why It Matters
SQLX’s native core combines strict metadata, smart caches, and SQLite extensions (UDFs, VTabs) to deliver a dataset that’s both UI-aware and transaction-safe. Lookups render foreign keys as human-readable values without extra queries; pivots become a single SQL construct; auditing is synthesized with readable diffs and user attribution. The result is a high-leverage data layer that preserves ADO ergonomics while exploiting modern SQLite capabilities.


5.3 ORMX.js – Object Relational Mapper

extension demo

5.3.1 Overview & Goals

ORMBase lifts SQLX datasets into ergonomic JavaScript objects using three core mechanisms:

An instantiated ORM object has a dual nature: it represents the current record view and it is also a generator usable in for…of to stream records.

ORM Master-Detail Business Object Sample:

class User extends SQLX.ORMBase
{
    constructor()
    {
        super();
        this.meta.Class = User;
        this.meta.TableName = '[Users]';
        this.meta.ID = '[Users].[UserID]';
        this.meta.Limit = 0;

        this.AddDetail(UserCredentials, 
          '[UserCredentials]', 
          '[UserCredentials].[ID]',
          [], 
          '[Users].[UserID]', 
          '[UserCredentials].[UserID]');

        this.Meta();
    }

    get HasCredentials()
    {
        for (const cred of this.UserCredentials)
            return true;

        return false;
    }

    InsertUserCredential({ UserID, CredentialID, PublicKey, SignCount, Transports })
    {
        const cred = new UserCredentials();
        cred.Insert();
        cred.UserID = UserID;
        cred.CredentialID = CredentialID;
        cred.PublicKey = PublicKey;
        cred.SignCount = SignCount;
        cred.Transports = Transports;
        cred.Write();
    }    
};

5.3.2 Datastream Transport (Backstage ⇄ Browser)

ORM objects serialize their bound SQLX datasets into a binary Datastream for fast round-trips. This keeps metadata, rows, state (insert/update/delete), and constraints in one compact payload.

On the client-side (browser), we use an EMSCRIPTEN port of the Node.js Native Dataset. Thus, we can rely on same Dataset API on both the server and the client tiers.

Server: Backstage Model Fetch & Update Endpoints

// Client performs GET/POST to ModelData,
// the function returns a binary Datastream with the requested model
async function ModelData() {
 const { Arguments, Filters: rawFilters = [] } = Request.Body
  ? JSON.parse(Request.Body)
  : {}

 const Filters = rawFilters.map((f) => new SQLX.FILTER(f))
 const modelId = Request.ServerVariables('HTTP_X_SQLX_MODEL_ID')
 const property = Request.ServerVariables('HTTP_X_SQLX_MODEL_PROPERTY')

 let model = new MyApp.CreateModel(modelId, Arguments)

 if (Filters.length) model.meta.Filters = [...model.meta.Filters, ...Filters]

 // If model set length is small, prefetch
 if (model.Length <= 1) await model.Read()

 // Resolve nested property if requested (e.g. master-detail)
 if (property && property !== 'undefined')
  model = property.split('.').reduce((o, k) => o?.[k], model)

 // Serialize dataset (data + metadata + state)
 Response.ContentType = 'application/octet-stream'
 Response.BinaryWrite(model.dataset.toBytes())
 Response.End()
}

// Client performs POST to ModelUpdate,
// accepts a binary Datastream with changes;
// applies constraints and returns requery
async function ModelUpdate() {
 const ab = Request.BinaryRead() // ArrayBuffer

 const dataset = Server.CreateObject('SQLX.Dataset')
 dataset.UserSignature = Session.SessionID
 dataset.AuditUser = Session.AuditUser

 if (!dataset.loadBytes(ab)) throw new Error('Invalid DataStream')

 dataset.RequeryOnUpdate = true // return fresh rows after update
 dataset.EnableMetadataLookupConstraints = true // enforce FK, unique, ranges, etc.

 if (!(await dataset.update())) throw new Error(dataset.getLastError())

 // Send updated snapshot
 Response.ContentType = 'application/octet-stream'
 Response.BinaryWrite(dataset.toBytes())
 Response.End()
}

Client: DataBind Fetch & Update HTTP Calls

// Browse: POST to Backstage,
// Receive binary Datastream,
// Write Datastream to Emscripten Dataset,
// Refresh UI bindings.
async function Browse(dataset, options = {}) {
 dataset.FetchOptions = options // remember last fetch options

 try {
  // Build URL + search params (e.g. paging, search form fields)
  const url = new URL(
   `${this.options.URL}?method=${options.method || 'data'}`, window.location.origin
  )
  if (options.search)
   for (const [k, v] of Object.entries(options.search))
    url.searchParams.append(k, v)

  // Pass model/property via headers to the Backstage endpoint
  const headers = {
   'Content-Type': 'application/json',
   'x-csrf-token': this.options.CSRF,
   'x-sqlx-model-id': options.modelId, // e.g. 'Reservation'
   'x-sqlx-model-property': options.property || '', // e.g. 'Details.Payments'
   ...options.headers
  }

  // Optional server-side arguments/filters
  const body = JSON.stringify({
   Arguments: options.arguments || {},
   Filters: options.filters || [] // client-proposed filters (server still enforces)
  })

  const res = await fetch(url.toString(), { method: 'POST', headers, body })
  if (!res.ok) {
   if (res.status === 520) throw new Error((await res.json()).message)
   throw new Error(`HTTP ${res.status}`)
  }

  // Hydrate dataset from binary stream
  const ui8 = new Uint8Array(await res.arrayBuffer())
  if (!(await dataset.write(ui8))) throw new Error('Invalid DataStream')

  // Repaint DataGrids, forms, etc.
  await this.RefreshBindings(dataset)

 } catch (e) {
  w2utils.notify(('Fetch failed\n' + e.message).replace(/\n/g, '<br>'), { error: true })
 } 

 return this
}

// Save: POST dataset deltas as binary to Backstage,
// Server applies constraints and returns re-queried data snapshot.
async function Save(dataset) {
 try {
  // Read current change set (minimal deltas) as Uint8Array
  const ui8 = dataset.read()

  const res = await fetch(`${this.options.URL}?method=update`, {
   method: 'POST',
   headers: {
    'x-csrf-token': this.options.CSRF,
    'Content-Type': 'application/octet-stream'
   },
   body: ui8.buffer
  })

  if (!res.ok) {
   if (res.status === 520) throw new Error((await res.json()).message)
   throw new Error(`HTTP ${res.status}`)
  }

  // Reload dataset from server’s post-update snapshot (with RequeryOnUpdate)
  const uui8 = new Uint8Array(await res.arrayBuffer())
  await dataset.write(uui8)

  // Refresh UI Data Bindings
  await this.RefreshBindings(
   dataset,
   /*rebind*/ false,
   /*preserveSelection*/ true
  )

  w2utils.notify(`Saved OK. Fetched ${dataset.recordCount} records.`, { timeout: 2000 })
 } catch (e) {
  w2utils.notify(('Update failed\n' + e.message).replace(/\n/g, '<br>'), { error: true })
 } 

 return this
}

5.3.3 Dynamic SQL Engine (Filters & SHAPE)

5.3.4 Dynamic Field Wrappers (Auto Getters/Setters)

5.3.5 Dynamic Detail Wrappers (Iterators + Active-Record View)

For every declared detail:

5.3.6 The ORM Proxy (Why it’s required & how it’s used)

Every ORM instance is returned through a Proxy to enforce shape and enable advanced behavior:

class ORMBase extends EventEmitter
{
    /** @type {import('./@types/Dataset').CocoDataset} */
    dataset = null;

    /** @type {boolean} Wrap an existing dataset */
    LINKED_DATASET = false;

    /** @type {DESCRIPTOR} */
    meta = null;

    /** @type {AI_DESCRIPTOR} */
    openai = null;
    
    /** @type {boolean} suppresses sealing when setting dynamic properties */
    __internalDefine = false;

    /***************************************************************************************
     * Initializes a new instance of the ORM base class.
     * - Sets up metadata, dataset, and details as non-enumerable properties.
     * - Ensures that a valid connection string is defined before proceeding.
     *
     * @param {string} TableName - The name of the table associated with the ORM instance.
     * @param {string} ID - The primary key column name for the table.
     * @param {FILTER[]} [Filters=[]] - An array of filters to apply to the table.
     * @throws {Exception} - If the global `Database.ConnectionString` is not defined.
     */
    constructor(...args)
    {
        super();

        // Detect inheritance signature: `super(this, TableName, ID, Filters)`
        if (args[0] && typeof (args[0]) === "function" && args[0].prototype instanceof ORMBase)
        {
            const [Class, TableName, ID, Filters = []] = args;
            this.__constructor(Class, TableName, ID, Filters);
        }
        else
        {
            // Stand-alone signature: `new ORMBase(TableName, ID, Filters)`
            const [TableName, ID, Filters = []] = args;
            this.__constructor(ORMBase, TableName, ID, Filters);
        }

        this.__sealed = false;

        // Wrap the object in a Proxy to prevent adding new properties
        return new Proxy(this, {

            get(target, prop, receiver)
            {
                if (prop === "$proxy_target") return target;

                // Enable `for (let record of instance)` by handling Symbol.iterator
                // Only inject an iterator if target hasn't defined one itself
                if (prop === Symbol.iterator && !Reflect.has(target, Symbol.iterator))
                {
                    return function* ()
                    {
                        target.First();
                        while (!target.EOF)
                        {
                            yield target;
                            target.Next();
                        }
                    };
                }

                return Reflect.get(target, prop, receiver);
            },

            set(target, prop, value, receiver)
            {
                if (target.__sealed && !(prop in target))
                {
                    const name = target.constructor.name;
                    const msg = `⛔ Data Field "${prop}" not in ORM ${name} instance.`;
                    console.log(msg);
                    throw new Exception(msg);
                }
                // Respect accessors and prototype chain
                return Reflect.set(target, prop, value, receiver);
            },

            deleteProperty(target, prop)
            {
                const name = target.constructor.name;
                const msg = `⛔ Cannot delete Data Field property "${prop}" from ORM ${name} instance.`;
                console.log(msg);
                throw new Exception(msg);
            },

            ownKeys(target)
            {
                return Reflect.ownKeys(target);
            },

            getOwnPropertyDescriptor(target, prop)
            {
                return Reflect.getOwnPropertyDescriptor(target, prop);
            },

            has(target, prop)
            {
                return Reflect.has(target, prop);
            },

            defineProperty(target, prop, descriptor)
            {
                // 1) Always allow symbols (debug hooks, proxy escape hatch, etc.)
                if (typeof prop === 'symbol')
                {
                    return Reflect.defineProperty(target, prop, descriptor);
                }

                // 2) Allow known internal properties used by dynamic detail objects
                if (prop === 'Parent' || prop === '__MasterID' || prop === '__DetailID')
                {
                    return Reflect.defineProperty(target, prop, descriptor);
                }

                // 3) Allow framework-initiated defines while building wrappers
                if (target.__internalDefine === true)
                {
                    return Reflect.defineProperty(target, prop, descriptor);
                }

                // 4) If redefining an existing configurable property, allow it
                const existing = Reflect.getOwnPropertyDescriptor(target, prop);
                if (existing && existing.configurable)
                {
                    return Reflect.defineProperty(target, prop, descriptor);
                }

                // 5) Enforce sealing for any *new* non-symbol props when sealed
                if (target.__sealed && !(prop in target))
                {
                    const name = target.constructor.name;
                    throw new Exception(`⛔ Data Field "${String(prop)}" not in ORM ${name} instance.`);
                }

                return Reflect.defineProperty(target, prop, descriptor);
            },
        });
        ...
    }
}

This Proxy is essential: it preserves a stable, metadata-driven object model while still supporting streaming iteration and controlled augmentation for details.

5.3.7 Dataset Wiring & Lookups (at Read time)

Before/while reading:

5.3.8 Error-Handling Philosophy

💡 Why It Matters You write business objects, not plumbing: columns become safe properties with rules derived from metadata; details behave like typed collections. The Proxy gives you the best of both worlds: a sealed, predictable object model and generator-style iteration for streaming large result sets. The SQL engine handles real-world composition (nested filters, alias-aware templates, SHAPE), while lookup/caching integrates cleanly with UI and validation.

5.4 ORM Examples

5.4.1 Smallest possible ORM (table → iterate)

class Country extends SQLX.ORMBase {
 constructor() {
  super()
  this.meta.Class = Country
  this.meta.TableName = '[Countries]'
  this.meta.ID = '[Countries].[CountryID]'
  this.meta.Orders = [new SQLX.ORDER_BY('[Countries].[Name]', 'ASC')]
  this.meta.Limit = 0
  this.Meta() // load metadata only (no rows yet)
 }
};

// read & stream rows with dual-nature iterator
const countries = new Country()
countries.ReadAll() // open dataset
for (const c of countries) {
  // uses Proxy+iterator over cursor
  console.log(c.CountryID, c.Name)
}

5.4.2 Filtered read (WHERE compiler, limit/order)

class Mailer extends SQLX.ORMBase {
 constructor(nameOrId) {
  super()
  this.meta.Class = Mailer
  this.meta.TableName = '[EmailAccounts]'
  this.meta.ID = '[EmailAccounts].[EmailID]'
  this.meta.Limit = 0

  if (typeof nameOrId === 'number') {
   this.ReadByID(nameOrId)
  } else if (typeof nameOrId === 'string') {
   this.meta.Filters = [
    new SQLX.FILTER('[EmailAccounts].[Identifier]', '=', nameOrId),
    new SQLX.FILTER('[EmailAccounts].[IsActive]', '=', '1')
   ]
   this.Read()
  } else {
   this.ReadAll()
  }
 }
};

5.4.3 Insert / Update with dynamic field wrappers

class Artist extends SQLX.ORMBase {
 constructor() {
  super()
  this.meta.Class = Artist
  this.meta.TableName = '[Artists]'
  this.meta.ID = '[Artists].[ArtistID]'
  this.meta.Limit = 0
  this.Meta() // define getters/setters for columns
 }
};

const artist = new Artist()

// INSERT
artist.Insert()
artist.ArtistName = 'New Artist' // setter writes to dataset column (editable-only)
artist.Write() // commit

// UPDATE (load → edit → write)
artist.ReadByID(1)
artist.Edit()
artist.Genre = 'House'
artist.Write()

5.4.4 JOINs + calculated fields (same shape as EventItem)

class EventItem extends SQLX.ORMBase {
 constructor() {
  super()
  this.meta.Class = EventItem
  this.meta.TableName = '[EventItems]'
  this.meta.ID = '[EventItems].[EventItemID]'
  this.meta.Limit = 0

  // JOIN Items
  this.meta.Joins = [
   new SQLX.JOIN('INNER', '[Items]', [
    new SQLX.FILTER('[EventItems].[ItemID]', '=', '[Items].[ItemID]')
   ])
  ]

  // Calculated field (server-side expression)
  this.meta.Calculated = [
   `
            COALESCE((
                SELECT SUM(1 + COALESCE(SPLITCOUNT([OtherNames], '|'), 0))
                FROM [Reservations]
                WHERE [Reservations].[Status] = 'CAPTURED'
                AND [Reservations].[EventID] = [EventItems].[EventID]
                AND [Reservations].[ItemType] = [Items].[Type]
            ), 0) AS [ReservedTickets]
            `
  ]

  this.Meta()
 }
};

// Example: read items for one event date (via lookup filter)
const items = new EventItem()

items.meta.LookupFilters = {
 '[EventItems].[EventID]': [
  new SQLX.FILTER('[Events].[EventDate]', 'LIKE', `%${FISCAL_YEAR}%`)
 ]
}

items.ReadAll()

for (const it of items) {
 console.log(it.EventItemID, it.ItemID, it.ReservedTickets)
}

5.4.5 Master–Detail (SHAPE): Event → EventItems (+ JOIN) & EventArtists

This mirrors your Event model with two details and extra options.

class Event extends SQLX.ORMBase {
 constructor(idOrDate) {
  super()
  this.meta.Class = Event
  this.meta.TableName = '[Events]'
  this.meta.ID = '[Events].[EventID]'
  this.meta.Limit = 0
  this.meta.Filters = [
   new SQLX.FILTER('[Events].[EventDate]', 'LIKE', `%${FISCAL_YEAR}%`)
  ]
  this.meta.Orders = [new SQLX.ORDER_BY('[Events].[EventDate]', 'ASC')]

  // Detail 1: EventItems joined with Items
  const D1 = this.AddDetail(
   EventItem,
   '[EventItems]',
   '[EventItems].[EventItemID]',
   [],
   '[Events].[EventID]',
   '[EventItems].[EventID]'
  )
  D1.LookupFilters = {
   '[EventItems].[EventID]': [
    new SQLX.FILTER('[Events].[EventDate]', 'LIKE', `%${FISCAL_YEAR}%`)
   ]
  }
  D1.Orders = [new SQLX.ORDER_BY('[Items].[Sort]', 'ASC')]
  D1.Joins = [
   new SQLX.JOIN('INNER', '[Items]', [
    new SQLX.FILTER('[EventItems].[ItemID]', '=', '[Items].[ItemID]')
   ])
  ]
  D1.Fields = ['[EventItems].*', '[Items].*']

  // Detail 2: EventArtists
  const D2 = this.AddDetail(
   EventArtist,
   '[EventArtists]',
   '[EventArtists].[EventArtistsID]',
   [],
   '[Events].[EventID]',
   '[EventArtists].[EventID]'
  )
  D2.LookupFilters = {
   '[EventArtists].[EventID]': [
    new SQLX.FILTER('[Events].[EventDate]', 'LIKE', `%${FISCAL_YEAR}%`)
   ]
  }

  if (typeof idOrDate === 'number') this.ReadByID(idOrDate)
  else if (typeof idOrDate === 'string') this.ReadByDate(idOrDate)
  else this.Meta()
 }

 ReadByDate(date) {
  this.meta.ID_VALUE = null
  this.meta.Filters = [
   new SQLX.FILTER(
    '[Events].[EventDate]',
    'DATE',
    new Date(date).toISOString().split('T')[0]
   )
  ]
  this.Read()
 }
};

// Iterate masters and details (dual-nature object + iterable detail proxies)
const events = new Event()
events.ReadAll()
for (const ev of events) {
 console.log('Event:', ev.EventID, ev.EventDate)
 for (const row of ev.EventItems) {
  // detail iterator (shared handle)
  console.log('  Item:', row.ItemID, row.Item?.Type, row.ReservedTickets)
 }
 for (const ea of ev.EventArtists) {
  console.log('  Artist:', ea.ArtistID, ea.Artist?.ArtistName)
 }
}

5.4.6 Dictionary model (key/value via metadata)

class Settings extends SQLX.ORMBase {
 constructor() {
  super()
  this.meta.Class = Settings
  this.meta.TableName = '[Settings]'
  this.meta.ID = '[Settings].[ID]'
  this.meta.Limit = 0
  this.meta.isDictionary = true
  this.meta.KEY = 'Name'
  this.meta.VALUE = 'Value'
  this.ReadAll()
  this.RegisterVars() // publish key→value into ReplaceVars pipeline / globals
 }
}

5.4.7 Custom SQL / View models (pivot-friendly)

class CampaignResultsPivot extends SQLX.ORMBase {
 constructor() {
  super()
  this.meta.Class = CampaignResultsPivot
  this.meta.TableName = '[CampaignResultsPivot]'
  this.meta.ID = '[CampaignResultsPivot].[CampaignID]'
  this.meta.IsView = true
  this.meta.Limit = 0
  this.SetCustomSQL(`
            WITH P AS ( SELECT * FROM PIVOT(CampaignResults, CampaignID, Value, 1) )
            SELECT C.[Name],
                    PIVOT_COLUMNS(P.*, CampaignID),
                    PIVOT_SUM(P.*, CampaignID, [no])   AS [Positive],
                    PIVOT_SUM(P.*, CampaignID)         AS [Total]
            FROM P JOIN Campaigns C ON C.[ID] = P.[CampaignID];
        `)
  this.Meta()
 }
}

5.4.8 Validation before Write() (required fields, formats, FK sanity)

class Artist extends SQLX.ORMBase {
 constructor() {
  super()
  this.meta.Class = Artist
  this.meta.TableName = '[Artists]'
  this.meta.ID = '[Artists].[ArtistID]'
  this.meta.Limit = 0
  this.Meta() // define dynamic getters/setters
 }

 // Minimal, explicit validation (add your own rules here)
 validate() {
  const errs = []

  // Required fields (example)
  if (!this.ArtistName || !this.ArtistName.trim())
   errs.push('ArtistName is required.')

  // Format check
  if (this.Website && !/^https?:\/\//i.test(this.Website))
   errs.push('Website must be http(s) URL.')

  // FK presence (optional, SHAPE/constraints also guard this on commit)
  if (this.GenreID == null) errs.push('Genre is required.')

  // Uniqueness (simple example check)
  const dup = SQLX.Run(
   `
            SELECT 1 FROM [Artists] WHERE [ArtistName] = ? AND [ArtistID] <> ? LIMIT 1
            `,
   this.ArtistName,
   this.ArtistID || 0
  )

  if (!dup.EOF) errs.push('ArtistName must be unique.')
  if (errs.length) throw new Error(errs.join('\n'))
 }
}

// Usage
const a = new Artist()
a.Insert()
a.ArtistName = 'New Artist'
a.GenreID = 3
a.Website = 'https://example.com'
a.validate() // throws if invalid
a.Write() // commit (FK/lookup constraints will also enforce DB rules)

Notes

5.4.9 Linked-object accessors (lazy getters vs. JOINs)

Lazy getter (no JOIN; fetch on demand)

class Item extends SQLX.ORMBase {
 constructor() {
  super()
  this.meta.Class = Item
  this.meta.TableName = '[Items]'
  this.meta.ID = '[Items].[ItemID]'
  this.Meta()
 }
}

class EventItem extends SQLX.ORMBase {
 constructor() {
  super()
  this.meta.Class = EventItem
  this.meta.TableName = '[EventItems]'
  this.meta.ID = '[EventItems].[EventItemID]'
  this.meta.Limit = 0
  this.Meta() // dynamic field wrappers
 }

 // Linked object: load when accessed
 get Item() {
  if (this.ItemID == null) return null
  const it = new Item()
  it.ReadByID(this.ItemID)
  return it
 }
}

// Usage
const evItems = new EventItem()
evItems.ReadAll()
for (const ei of evItems) {
 console.log(ei.EventItemID, ei.Item?.Type) // Item fetched on demand
}

JOIN-backed fields (no extra fetch; fields already present)

class EventItemWithJoin extends SQLX.ORMBase {
 constructor() {
  super()
  this.meta.Class = EventItemWithJoin
  this.meta.TableName = '[EventItems]'
  this.meta.ID = '[EventItems].[EventItemID]'
  this.meta.Joins = [
   new SQLX.JOIN('INNER', '[Items]', [
    new SQLX.FILTER('[EventItems].[ItemID]', '=', '[Items].[ItemID]')
   ])
  ]
  this.Meta()
 }

 // “Linked” view via already-joined columns (no ReadByID needed)
 get ItemType() {
  return this['Items.Type']
 }
}

// Usage
const evItems = new EventItemWithJoin()
evItems.ReadAll()
for (const ei of evItems) {
 console.log(ei.EventItemID, ei.ItemType) // zero extra queries
}

When to use which

6. Part VI – Utilities & Cross-Cutting Concerns

Contents

6.1 UTILX.js – Utility Functions

Objectives

Module Surface (Exports & Augmentations)

Design Notes & Behaviors

String templating with String.prototype.$(context)

Name normalization with NameCase(str)

Input hardening with SanitizeStringValue(input)

SQL stringification with StringifyValue(value, sanitize = true)

Caption generation with Caption(name)

Usage Examples

String templating

class Reservation { constructor() { this.Event = { EventDate: '2025-09-01', Venue: 'Cavo' }; } }
const r = new Reservation();
"Event: ${Reservation.Event.EventDate} @ ${Reservation.Event.Venue}".$(r); // "Event: 2025-09-01 @ Cavo"
"Wrong: ${Order.Id}".$(r); // ""

Safe value building for ad‑hoc SQL

const whereName = StringifyValue("O'Reilly");            // '\'O\'\'Reilly\''
const whereIds  = StringifyValue([1, 2, 3]);             // '(1, 2, 3)'
const when      = StringifyValue(new Date(2025, 7, 25)); // '2025-08-25 00:00:00' (example)

Defensive sanitization

const s = SanitizeStringValue("UNION SELECT password FROM Users");
if (s.startsWith("INVALID")) { throw new Error(s); }     // "INVALID STRING: SQL INJECTION"

Captioning fields for UI

Caption("customer_name");      // "Customer Name"
Caption("shortMonths");        // "Short Months"
Caption("userGUID");           // "User GUID"
Caption("(Already Formatted)");// "(Already Formatted)"

Integration & Best Practices

Caveats & Edge Cases

💡 Why It Matters UTILX.js centralizes tiny but high‑leverage primitives that recur across the stack: templating for labels, consistent UI captions, strict value formatting for SQL, and an opinionated string sanitizer. Keeping these concerns small, predictable, and reused reduces boilerplate, aligns behavior between modules, and shrinks the surface for injection bugs—especially in those few places where dynamic SQL is unavoidable.


7. Part VII – Backstage Framework (Backoffice MMV)

Contents

7.1 Overview of Backstage

aspjs-layered-technology-stack

Objectives

Architecture at a Glance

Is Backstage MVC Framework?

Innovations Beyond SPA Frameworks

Differences vs Classic ASP & Modern SPA Frameworks

💡 Why It Matters Backstage flips the script: instead of wasting weeks wiring up REST APIs, controllers, reducers, and Angular forms, you focus only on your models and metadata. The framework gives you:

  • A complete secure backoffice in hours, not months.
  • A cross-tier debugger that follows execution seamlessly from browser → server → browser again.
  • End-to-end architecture where server, client, and security are not patched together — they are designed as one. Backstage is not just another framework; it’s a radical acceleration layer for building business-critical applications with less code, more security, and unmatched debug visibility.

7.2 Server-Side Architecture

7.2.1 Session Bootstrapping & Model Preload (default.asp)

Objectives

Lifecycle

  1. Session check

    • On every request, default.asp verifies if a session token is present and valid.
    • If not, user is redirected to the secure login flow (webauthn.asp).
  2. Model preload

    • Once authenticated, core ORMX models are instantiated (e.g., Users, UserRights, Settings, Audit).
    • Business-specific models (Mailers, Payment Gateways, custom Entities) are also initialized.
  3. Metadata load

    • Finally, backstage.inc is included, which drives the rendering of the user interface.
    • All subsequent UI rendering and Remote Function Call execution is shaped by this metadata.

Security Considerations

Innovations Beyond SPA Frameworks

Differences vs Classic ASP & Modern SPA Frameworks

💡 Why It Matters default.asp ensures that every Backstage request starts secure and consistent: sessions are validated, models are ready, metadata is loaded. Developers don’t waste time wiring login checks, preload calls, or boilerplate state hydration. Instead, you immediately step into building business logic — with security already guaranteed.


7.2.2 Application Metadata & UI Definition (backstage.inc)

Purpose

What backstage.inc Produces

Security & Transport

Runtime Behavior (how the client consumes the metadata)

Remote Function Calls (RFC)

Developer Ergonomics

Differences vs Classic ASP & Modern SPA Frameworks

💡 Why It Matters With backstage.inc you don’t “code a screen,” you declare a screen. The client binder turns that declaration into a secure, rights‑aware UI with datasets, grids, charts, and RFC buttons—complete with CSRF, session‑encrypted action IDs, progress for long jobs, and file downloads. You get enterprise‑grade wiring (master→detail, search overlays, JSON config editors) without writing glue, and it all plays perfectly with Backstage’s dual debugger: step a toolbar click, hop into the server handler, and step right back into the client when the response lands. That’s RAD without compromise.


7.2.3 Remote Function Call Handlers (commands.inc)

Purpose

Execution Flow (end‑to‑end)

Handler Shape (typical patterns)

Input Types (what the client can send you)

Output Types (what you can return)

Security & Auditing

Developer Ergonomics

Differences vs Classic ASP & Modern SPA Frameworks

💡 Why It Matters commands.inc is where business power meets framework discipline. You expose operations as small, testable functions; the framework supplies the rest—secure dispatch, CSRF, model scoping, selected‑row payloads, server‑rendered forms, streaming exports, and long‑task progress. Compared to Classic ASP or modern SPA stacks, it’s dramatically less boilerplate, more secure, and easier to reason about—and with Backstage’s cross‑tier Dual Debugger you can click a button, step into the server handler, and land back in the client as soon as the response arrives.


7.2.4 Authentication & Security Endpoints (webauthn.asp, gmail.asp, oauth2.asp, etc.)

Objectives

Endpoint Roles

Security Model (Cross‑Cutting)

Operational Behaviors

Innovations Beyond SPA Frameworks

Differences vs Classic ASP & Modern SPA Frameworks

💡 Why It Matters Authentication and integration are where many admin apps grow fragile. Backstage’s dedicated endpoints establish a single, durable trust boundary (ASP Session + CSRF + opaque actions) and keep OAuth tokens server‑side only. You get WebAuthn‑grade security, simpler code, and fewer moving parts—so you can focus on business features without risking your security posture.


7.3 Client-Side Architecture

7.3.1 UI Loader & Initialization (default.asp)

Objectives

Runtime Flow

Differences vs Classic ASP & Modern SPA Frameworks

💡 Why It Matters A one-shot loader plus a smart binder means instant boot, fewer moving parts, and zero “app wiring” code. You go from login → fully rendered, secured backoffice in one hop.


7.3.2 Data Circuits & Binding Engine (databinding.js)

Objectives

Key Responsibilities

Differences vs Classic ASP & Modern SPA Frameworks

💡 Why It Matters The binder is where Backstage earns its speed: one engine takes a secure, session-shaped JSON and materializes a complete application—layouts, filters, charts, and all—while keeping calls safe (CSRF, scoped headers) and surfacing long-task progress out of the box.


7.3.3 Interactive Data Presentation (datagrid.js)

extension demo

Objectives

Surface & Options

Notable Behaviors

Differences vs Classic ASP & Modern SPA Frameworks

💡 Why It Matters DataGrid isn’t just a table — it’s the Backstage “view engine.” With one controller you jump from grid to form to calendar/email/chart without losing binding, selection, or context. Analysts get real keyboard power and one‑click exports; developers get rich context menus, bulk‑edit & paste, lookup popups, uploads, and first‑class charting with sensible defaults. It feels like a desktop client… in a browser… wired to your WebASM dataset.


7.3.4 UI Helpers & Widgets (sqlxems.js, w2ui.es6.js, multiselect.js, calendarpicker.js)

Objectives

Role in the Stack

Differences vs Classic ASP & Modern SPA Frameworks

💡 Why It Matters These helpers are purpose-built for Backstage’s binder and controllers. They enable complex screens (nested layouts, filters, calendars, multiselect search) to be declared in metadata and realized faithfully, without bespoke component code each time.


7.4 Data Binding & Controllers

Focus: CreateLayout (from databinding.js)

Objectives

What CreateLayout Does

Key Helpers Around CreateLayout

Algorithm (condensed)

  1. For each panel in layout.panels:
    • Resolve active tab → locate boundControl (panel‑level or tab‑level).
    • If noneRenderPlainTab.
    • If exists → ensure dataset is hooked; create/rehydrate controller; bind dataset; (re)build toolbar; refresh if needed.
  2. For each panel with a child layout, recurse (CreateLayout(child)).
  3. When rendering to DOM, call RenderLayout to construct w2layout, install ChartLayout toolbars if present, and relink descriptors ↔ runtime objects.
  4. After the tree is mounted, refresh bindings; optionally open Report if a chart side‑panel is configured.

Master→Detail Cohesion

Quality‑of‑Life Details

Differences vs Classic ASP & Modern SPA Frameworks

💡 Why It Matters CreateLayout is where Backstage’s metadata becomes a living app. With one pass it resolves bound controls, hooks datasets, binds controllers, renders nested layouts, wires tabs, installs report toolbars, and guarantees detail views track the master—then tears it all down cleanly when you navigate. It’s the reason you don’t need a SPA router, store, or hand‑written wiring: the binder does it once, the same way, everywhere, and your team ships features instead of scaffolding.

7.5 UI Components & Views

7.5.1 Grid View

The Grid View is the default presentation of datasets in Backstage. It offers:

extension demo

7.5.2 Form & Search Views

The Form View transforms a single record into a structured form:

extension demo

The Search Form View provides query building:

extension demo

7.5.3 List & Calendar Views

List View displays records as rich cards or thumbnails:

extension demo

Calendar View organizes records over time:

extension demo

7.5.4 Email View

The Email View renders dataset records as styled message pages:

7.5.5 Chart Views

extension demo

The Chart View brings visual analytics to datasets using ECharts:

7.6 Security & Remote Calls

Objectives

Client–Server Mechanics (What the binder actually does)

Headers, Tokens & Scoping

Payload Shaping & Selected Records

Long‑Running Tasks (Progress UI)

Binary Responses (File Downloads)

Result Handling & Post‑Actions

Errors & Guardrails

Master→Detail Security & Consistency

Developer Tips (Using it well)

💡 Why It Matters Backstage’s binder turns security into a contract: every call ships CSRF, model scope, and opaque action IDs; long jobs get progress UI; files download cleanly; errors surface consistently. You write business logic—the plumbing is guaranteed.


7.7 Extensibility & Business Integration

7.7.1 What we’re building

This hands-on mini-project shows how to build an RSVP feature with ASP.js Backstage on top of SQLX/ORMX. You’ll create the database, models, email templates, an endpoint to capture clicks, and a Backstage node (metadata) that renders a full back-office UI with a pivoted overview and a chart.

Flow (high level):

  1. Back-office user triggers an RSVP campaign (or your app does it automatically).

  2. The system sends templated emails containing “answer buttons” (YES / MAYBE / NO).

  3. Each button points to an endpoint like /endpoints/survey.asp?type=rsvp&campaign=…&guid=…&answer=yes.

  4. When a customer clicks, the endpoint writes a row in CampaignResults.

  5. The UI shows:

    • Master grid of Campaigns.
    • Detail grid of CampaignResults for the selected campaign.
    • A Pivot view (CampaignResultsPivot) summarizing answers.
    • A Chart on top of the pivot for visual insights.

7.7.2 Project Folder Structure

📄 server.js                               – Application entry point: initializes SiteManager and DB 
📄 package.json                            – Node.js project manifest
📄 config.json                             – Web server configuration: ports, SSL, AutoBan, Security
📂 node_modules                            – Installed Node.js dependencies 
 ┣ 📂 @mobilefx                            – mobileFX framework modules
 ┃ ┗ 📂 aspx                               – Core ASPX runtime (IISX, SITEX, HTTPX, etc.)
 ┗ 📂 @mycompany                           – Your company’s custom modules
   ┗ 📂 myapp                              – Your application package
     ┗ 📄 Model.js                         – ORM models and business logic
📂 .ssl                                    – SSL/TLS certificates and private keys
 ┣ 📂 .intermediate                        – Intermediate CA certificates (for trust chain)
 ┃  ┣ 📄 AAA_Certificate_Services.crt      – AAA Certificate Services intermediate certificate
 ┃  ┣ 📄 Sectigo_RSA_Domain_Validation.crt – Sectigo RSA Domain Validation intermediate certificate
 ┃  ┗ 📄 USERTrust_RSA_Authority.crt       – USERTrust RSA Certification Authority intermediate 
 ┣ 📂 backstage.example.com                – SSL/TLS material for Backstage back-office
 ┃  ┣ 📄 CACert.crt                        – Intermediate CA certificates (for trust chain)
 ┃  ┣ 📄 backstage_example_com.pem         – Site certificate
 ┃  ┗ 📄 backstage_example_com.key         – Site private key
 ┗ 📂 example.com                          – SSL/TLS material for public website
 ┃  ┣ 📄 CACert.crt                        – Intermediate CA certificates (for trust chain)
    ┣ 📄 www_example_com.pem               – Site certificate
    ┗ 📄 www_example_com.key               – Site private key
📂 sites                                   – Root folder for hosted sites
 ┣ 📂 .data                                – SQLite data stores
 ┃ ┣ 📄 ASPX.db                            – ASPX internal DB (sessions, AutoBan, Audit)
 ┃ ┗ 📄 MyDatabase.db                      – Application DB (business data)
 ┣ 📂 backstage.example.com                – Backstage back-office web application
 ┃ ┣ 📂 .inc                               – Server-side include files (metadata & logic)
 ┃ ┃ ┣ 📄 backstage.inc                    – Application metadata: layouts, datasets
 ┃ ┃ ┣ 📄 commands.inc                     – Remote function call (RFC) handlers
 ┃ ┃ ┗ 📄 login.inc                        – Authentication logic (WebAuthn)
 ┃ ┣ 📄 default.asp                        – Bootstrap ASP page for Backstage client UI
 ┃ ┣ 📄 manifest.json                      – Web App Manifest (PWA metadata: name, theme, icons)
 ┃ ┗ 📄 favicon.ico                        – Backstage site favicon
 ┗ 📂 example.com                          – Example frontend site
    ┣ 📄 default.asp                       – Simple ASP.js demo application page
    ┣ 📄 enpoint.asp                       – RSVP Endpoint
    ┗ 📄 favicon.ico                       – Example site favicon

7.7.3 Backstage Server (/server.js)

This is the entry point of your Backstage application. Here we define the database connection string, import our framework, initialize the site manager, and schedule background jobs.

// Import core ASPX classes
const { SiteManager, Site, Proxy } = require("@mobilefx/aspx"); 

 // Set default SQLite connection
global.SQLX.Database.ConnectionString = `${__dirname}/sites/.data/MyDatabase.db`;

 // Import your framework
global.Cavo = require("@mycompany/myapp");

// Initialize Site Manager
const siteManager = new SiteManager(); 

// Start all configured sites
siteManager.Start(); 

// Create CronJob instance
global.cronJobsInst = new Cavo.CronJob(); 

// Schedule all active cron jobs
global.cronJobsInst.ScheduleJobs(); 

7.7.4 Web Host (/config.json)

This JSON file defines how your Backstage server will run. It sets up the second database (ASPX.db) used by ASP.js framework itself, security checks, ports, proxies, and site definitions (including SSL certificates). The options section configures defaults like AutoBan, CSRF checks, and allowed directories/extensions, while the sites array maps each hostname to its corresponding web root and SSL setup. In this example, we’re hosting backstage.example.com over HTTPS with its own certificate.

{
  "crudpads": [],
  "options": {
    "database": "sites/.data/ASPX.db",
    "autoban": {  
      "blockListDatabase": "sites/.data/ASPX.db",      
      "learningMode": false
    },
    "httpPort": 80,
    "httpsPort": 443,
    "internalPort": 10000,
    "localhostMode": true,
    "siteDefaults": {
      "allowedDirectories": null,
      "allowedExtensions": null,
      "enable_csrf_check": false,
      "securityChecks": [
        "FLOOD_ATTACK",
        "METHOD_NOT_ALLOWED",
        "INVALID_URL",
        "INVALID_MIME",
        "DIRECTORY_DENIED",
        "DIRECTORY_INDEXING",
        "HIDDEN_PATH_ACCESS",
        "EXTENSION_DENIED",
        "ACCESS_DENIED",
        "INVALID_HOST",
        "INVALID_SITE"
      ]
    },
    "webRoot": "sites"
  },
  "proxies": [],
  "sites": [
    {
      "HTTP_404_URL": "/page-404.asp",
      "aliases": [
        {
          "host": "www.example.com",
          "ssl": {
            "ca": [
              ".ssl/example.com/CACert.crt"
            ],
            "cert": ".ssl/example.com/www_example_com.pem",
            "key": ".ssl/example.com/www_example_com.key"
          }
        }
      ],
      "host": "example.com",
      "path": "example.com",
      "ssl": {
        "ca": [
          ".ssl/example.com/CACert.crt"
        ],
        "cert": ".ssl/example.com/www_example_com.pem",
        "key": ".ssl/example.com/www_example_com.key"
      }
    },
    {
      "host": "backstage.example.com",
      "path": "backstage.example.com",
      "ssl": {
        "ca": [
          ".ssl/backstage.example.com/CACert.crt"
        ],
        "cert": ".ssl/backstage.example.com/backstage_example_com.pem",
        "key": ".ssl/backstage.example.com/backstage_example_com.key"
      }
    }              
  ]
}

7.7.5 Application Database

This schema defines the core tables needed for the RSVP feature:

Together these tables provide a full RSVP lifecycle: define a campaign, send messages, capture responses, and store results safely for reporting and pivots in Backstage.

💡Use mobileFX SQLite Express or any SQLite shell to run this DDL. The unique index prevents duplicate answers for the same (CampaignID, ReservationGUID, Tracker).

CREATE TABLE IF NOT EXISTS [Campaigns] (
 [ID] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ON CONFLICT ROLLBACK UNIQUE ON CONFLICT ROLLBACK ,
 [IsActive] BOOLEAN NOT NULL ON CONFLICT ROLLBACK DEFAULT (1) ,
 [Name] VARCHAR(50) NOT NULL ON CONFLICT ROLLBACK ,
 [Created] DATETIME NOT NULL ON CONFLICT ROLLBACK DEFAULT (GETDATETIME()) ,
 [ReachCount] INTEGER NOT NULL ON CONFLICT ROLLBACK DEFAULT (0) ,
 [EngagementCount] INTEGER NOT NULL ON CONFLICT ROLLBACK DEFAULT (0) ,
 [EmailMessage] HTML5 NOT NULL ON CONFLICT ROLLBACK ,
 [EngagementMessage] HTML5 NOT NULL ON CONFLICT ROLLBACK ,
 [ErrorMessage] HTML5 NOT NULL ON CONFLICT ROLLBACK );

CREATE TABLE IF NOT EXISTS [CampaignResults] (
 [ID] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ON CONFLICT ROLLBACK UNIQUE ON CONFLICT ROLLBACK ,
 [CampaignID] INTEGER NOT NULL ON CONFLICT ROLLBACK REFERENCES [Campaigns] ([ID]) ON UPDATE CASCADE ON DELETE CASCADE , --[CMETA:eyJMT09LVVBfVkFMVUVfQ09MVU1OIjogIltOYW1lXSJ9]
 [ResponseDate] DATETIME NOT NULL ON CONFLICT ROLLBACK DEFAULT (GETDATETIME()) ,
 [ReservationGUID] VARCHAR(50) NOT NULL ON CONFLICT ROLLBACK ,
 [Tracker] VARCHAR(50) NOT NULL ON CONFLICT ROLLBACK ,
 [Value] VARCHAR(50) NOT NULL ON CONFLICT ROLLBACK );

CREATE INDEX [INDEX_CampaignResults_CampaignID__Campaigns_ID] ON [CampaignResults] ([CampaignID]);

CREATE UNIQUE INDEX idx_campaignresults ON CampaignResults (CampaignID, ReservationGUID, Tracker);

CREATE TABLE IF NOT EXISTS [Settings] (
 [ID] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ON CONFLICT ROLLBACK UNIQUE ON CONFLICT ROLLBACK ,
 [Name] VARCHAR(50) NOT NULL ON CONFLICT ROLLBACK ,
 [Value] VARCHAR NOT NULL ON CONFLICT ROLLBACK );

CREATE TABLE IF NOT EXISTS [Templates] (
 [TemplateID] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ON CONFLICT ROLLBACK UNIQUE ON CONFLICT ROLLBACK ,
 [Name] VARCHAR(50) NOT NULL ON CONFLICT ROLLBACK ,
 [Text] HTML5 NOT NULL ON CONFLICT ROLLBACK ); 

7.7.6 Email Templates

The Templates table holds the HTML fragments that are injected into outgoing RSVP emails. Each template uses placeholders (${{Message}}) that will be substituted with campaign-specific text at runtime.

The DOMAIN_NAME (eg. backstage.example.com) constant defines the base URL where the survey endpoint is hosted — make sure it includes a trailing slash so generated links resolve correctly.

RSVP Template:

<div style="font-family:Arial,sans-serif">
  <p>${'{'}{Message}{'}'}</p>
  <p>Please respond:</p>
  <survey value="yes"  style="background-color:#28a745;width:160px;border-radius:6px">I’m coming</survey>
  <survey value="maybe" style="width:160px;border-radius:6px">Maybe</survey>
  <survey value="no"   style="background-color:#dc3545;width:160px;border-radius:6px">Can’t</survey>
</div>

Thank you Template:

<div style="font-family:Arial,sans-serif">
  <h2>Thanks!</h2>
  <p>${'{'}{Message}{'}'}</p>
</div>

Error Template:

```html
<div style="font-family:Arial,sans-serif">
  <h2>Sorry!</h2>
  <p>${'{'}{Message}{'}'}</p>
</div>
const DOMAIN_NAME = 'https://backstage.example.com/'; // trailing slash required

7.7.7 ORM Models (node_modules@mycompany/myapp/Models.js)

These ORM classes map directly to the RSVP schema and provide the business logic on top. They use SQLX.ORMBase to expose tables as live objects, automatically loading metadata, constraints, and detail relationships.

Together, these models allow you to send campaigns, capture results, enforce rules, and display analytics — all using the declarative ORM style rather than raw SQL.

const { SQLX } = require('@mobilefx/aspx');
const crypto = require('crypto');

class Templates extends SQLX.ORMBase
{
    constructor()
    {
        super();
        this.meta.Class = Templates;
        this.meta.TableName = '[Templates]';
        this.meta.ID = '[Templates].[TemplateID]';
        this.meta.Limit = 0;
        this.meta.isDictionary = true;
        this.meta.KEY = 'Name';
        this.meta.VALUE = 'Text';
        this.ReadAll();
        this.RegisterVars(); // Load records as key-value pairs in globalThis
    }
};

class Settings extends SQLX.ORMBase
{
    constructor()
    {
        super();
        this.meta.Class = Settings;
        this.meta.TableName = '[Settings]';
        this.meta.ID = '[Settings].[ID]';
        this.meta.Limit = 0;
        this.meta.isDictionary = true;
        this.meta.KEY = 'Name';
        this.meta.VALUE = 'Value';
        this.ReadAll();
        this.RegisterVars();
    }
};

class Campaign extends SQLX.ORMBase {
  constructor() {
    super();
    this.meta.Class = Campaign;
    this.meta.TableName = '[Campaigns]';
    this.meta.ID = '[Campaigns].[ID]';
    this.meta.Limit = 0;

    this.AddDetail(
      CampaignResults,
      "[CampaignResults]",
      "[CampaignResults].[ID]",
      [],
      "[Campaigns].[ID]",
      "[CampaignResults].[CampaignID]"
    );

    this.Meta();
  }
  
  static RSVP_Request(options) {
    const mailer = new Mailer(options.Mailer || 'RSVP');
    if (!mailer) throw new Exception('RSVP Mailer not found');

    const tEmail  = globalThis[options.TemplateEmail];
    const tThanks = globalThis[options.TemplateThankYou];
    const tError  = globalThis[options.TemplateError];
    if (!tEmail)  throw new Exception(`Template ${options.TemplateEmail} not found`);
    if (!tThanks) throw new Exception(`Template ${options.TemplateThankYou} not found`);
    if (!tError)  throw new Exception(`Template ${options.TemplateError} not found`);

    const campaignHash = crypto.createHash('md5').update(options.Campaign, 'utf8').digest('hex');

    const vars = Object.assign({}, options.Vars || {});
    const trackerVal = vars[options.TrackerKey || 'Tracker'] || '';
    const guidVal    = vars[options.GuidKey    || 'GUID']    || '';

    // Load or create campaign
    const campaign = new Campaign();
    campaign.ReadByName(options.Campaign);
    if (campaign.EOF) {
      campaign.Insert();
      campaign.Name = options.Campaign;

      // Build the email body from the template, convert <survey> tags to buttons
      let html = String(tEmail).replace('${{Message}}', options.MessageEmail || '');

      html = html.replace(/(?:\s*<survey[\s\S]*?<\/survey>\s*)+/gi, (group) => {
        const rxTag = /<survey\b([^>]*)>([\s\S]*?)<\/survey>/gi;
        const btns = [];
        let m;

        while ((m = rxTag.exec(group)) !== null) {
          const attrs = m[1] || '';
          const label = (m[2] || '').trim();

          const vm    = /(?:^|\s)value\s*=\s*"([^"]+)"/i.exec(attrs);
          const value = encodeURIComponent((vm && vm[1] || '').trim());

          const sm    = /(?:^|\s)style\s*=\s*"([^"]+)"/i.exec(attrs);
          const style = {};
          if (sm) {
            sm[1].split(';').forEach(rule => {
              const [k, v] = rule.split(':').map(s => s && s.trim());
              if (k && v) style[k.toLowerCase()] = v;
            });
          }

          const val    = decodeURIComponent(value).toLowerCase();
          const bg     = style['background-color'] || (val === 'yes' ? '#28a745' : val === 'no' ? '#dc3545' : '#007BFF');
          const color  = style['color'] || '#ffffff';
          const width  = style['width'] || '160px';
          const radius = style['border-radius'] || '4px';

          const url =
            `${DOMAIN_NAME}endpoints/survey.asp?type=rsvp` +
            `&campaign=${campaignHash}` +
            `&tracker=${encodeURIComponent(trackerVal)}` +
            `&guid=${encodeURIComponent(guidVal)}` +
            `&answer=${value}`;

          btns.push({ label, bg, color, width, radius, url });
        }

        let fragment = '<table border="0" cellspacing="0" cellpadding="0" style="margin:16px 0;"><tr>';
        btns.forEach((b, i) => {
          if (i > 0) fragment += '<td style="width:24px;">&nbsp;</td>';
          fragment += `
<td align="center">
  <!--[if mso]>
  <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"
               href="${b.url}" style="height:40px;v-text-anchor:middle;width:${b.width};"
               arcsize="10%" stroke="f" fillcolor="${b.bg}">
    <w:anchorlock/>
    <center style="color:${b.color};font-family:Arial,sans-serif;font-size:14px;font-weight:bold;">
      ${b.label}
    </center>
  </v:roundrect>
  <![endif]-->
  <!--[if !mso]><!-- -->
  <a href="${b.url}" target="_blank"
     style="background-color:${b.bg};border-radius:${b.radius};color:${b.color};display:inline-block;
            font-family:Arial,sans-serif;font-size:14px;font-weight:bold;line-height:40px;text-align:center;
            text-decoration:none;width:${b.width};-webkit-text-size-adjust:none;">
    ${b.label}
  </a>
  <!--<![endif]-->
</td>`;
        });
        fragment += '</tr></table>';
        return fragment;
      });

      // Persist templates at campaign level
      campaign.EmailMessage      = html;
      campaign.EngagementMessage = String(tThanks || options.MessageThankYou || '').replace('${{Message}}', options.MessageThankYou || '');
      campaign.ErrorMessage      = String(tError  || options.ErrorMessage    || '').replace('${{Message}}', options.ErrorMessage || '');
      campaign.Write();
    }

    // Personalize and send using ORM's substitution
    const personalized = campaign.ReplaceVars(campaign.EmailMessage, vars);
    
    // If your mailer/stack supports extracting inline attachments from HTML, keep this;
    // otherwise, pass options.attachments straight through.
    const attachments = options.attachments || [];

    mailer.Send({
      to: options.To,
      subject: options.Subject || 'Invitation',
      html: personalized,
      attachments
    });
  }
  
  static RSVP_Response() {
    try {
      const type    = Request.QueryString("type");
      const name    = Request.QueryString("campaign"); // md5 of Campaign.Name
      const tracker = Request.QueryString("tracker");  // arbitrary tracker
      const answer  = Request.QueryString("answer");   // yes/no/maybe
      const guid    = Request.QueryString("guid");     // arbitrary GUID (optional)

      if (type !== 'rsvp') throw new Exception('Invalid RSVP Campaign');

      // Find active campaign by md5(Name)
      const campaign = new Campaign();
      campaign.meta.Filters = [
        new SQLX.FILTER("md5\\([Campaigns].[Name]\\)", "=", name),
        new SQLX.FILTER("[Campaigns].[IsActive]", "=", 1)
      ];
      campaign.Read();
      if (campaign.EOF) throw new Exception(`Invalid RSVP Campaign '${name}'`);

      // Count engagement
      campaign.EngagementCount++;
      campaign.Write();

      // Insert row (unique index prevents duplicates)
      try {
        SQLX.Exec(
          'INSERT INTO CampaignResults (CampaignID, ReservationGUID, Tracker, Value) VALUES (?, ?, ?, ?)',
          [[campaign.ID, guid || '', tracker || '', answer || '']]
        );
      } catch (e) {
        throw new Exception(`Duplicate RSVP not allowed (${tracker || 'tracker'})`);
      }

      // Render Thank-You using campaign-level substitution only
      const ctx = { GUID: guid || '', Tracker: tracker || '', Answer: (answer || '').toUpperCase(), Campaign: campaign.Name };
      const body = campaign.ReplaceVars(campaign.EngagementMessage, ctx);

      Response.AddHeader('Content-Type', 'text/html; charset=utf-8');
      Response.Write(body);
    } catch (e) {
      Response.AddHeader('Content-Type', 'text/html; charset=utf-8');
      Response.Write(e.message);
    }
  }
}

// Raw clicks (detail of Campaign)
class CampaignResults extends SQLX.ORMBase
{
  constructor()
  {
    super();
    this.meta.Class = CampaignResults;
    this.meta.TableName = '[CampaignResults]';
    this.meta.ID = '[CampaignResults].[ID]';
    this.meta.Limit = 0;
    this.Meta();
  }
}

// Pivoted overview (yes/no/maybe counts per campaign)
class CampaignResultsPivot extends SQLX.ORMBase
{
  CUSTOM_SQL = `
    WITH P AS (
      SELECT * FROM PIVOT(CampaignResults, CampaignID, Value, 1)
    )
    SELECT
      C.[Name],
      PIVOT_COLUMNS(P.*, CampaignID),
      PIVOT_SUM(P.*, CampaignID, [no]) AS [Positive],
      PIVOT_SUM(P.*, CampaignID)       AS [Total]
    FROM P
    JOIN Campaigns C ON C.[ID] = P.[CampaignID];
  `;

  constructor()
  {
    super();
    this.meta.Class = CampaignResultsPivot;
    this.meta.TableName = '[CampaignResultsPivot]';
    this.meta.ID = '[CampaignResultsPivot].[CampaignID]';
    this.meta.IsView = true;
    this.meta.Limit = 0;

    this.SetCustomSQL(this.CUSTOM_SQL);
    this.Meta();
  }
}

PIVOT(CampaignResults, CampaignID, Value, 1) groups by CampaignID, spreads distinct Value (yes/no/maybe…) into columns, and fills with 1 (counts). PIVOT_COLUMNS expands those columns, PIVOT_SUM(...,[no]) sums a specific column, PIVOT_SUM(...) totals all pivots.

7.7.8 Endpoint Wiring

<%
const { Campaign } = require('@mycompany/myapp');
Campaign.RSVP_Response();    
Response.End(); 
%>

7.7.9 UI metadata (Backstage node)

This Backstage JSON node defines master and detail datasets, and binds them to DataGrids on a complex yet intuitive UI layout. The UI is divided in a main master area on the top and the details area on the bottom. Details are assigned to tabbed-panels.

Add the following to backstage.inc:

{
  id: 'rsvp-campaigns',
  text: 'RSVP Campaigns',
  icon: SVG('button-choice'),

  datasets: [       
    { model: 'Campaign', type: 'master', name: 'Campaign', options: { showFilters: true, showAdvancedFetch: true } },       
    { model: 'Campaign', type: 'detail', master: 'Campaign', name: '[CampaignResults]', options: { showFilters: true } },
    { model: 'CampaignResultsPivot', type: 'master', name: 'CampaignResultsPivot', options: { showFilters: true, readOnly: true } },
  ],

  layout: {
    panels: [
    {
      type: 'main',
      style: 'overflow:hidden',
      resizable: true,
      hidden: false,
      layout: {
      panels: [
        {            
        type: 'main',
        style: 'overflow:hidden',
        resizable: true,
        hidden: false,
        boundControl: {
          dataset: 'Campaign',
          options: { type: 'grid' }
        }
        },
        {            
        type: 'bottom',
        size: '70%',
        style: 'overflow:hidden',
        resizable: true,
        hidden: false,
        tabs: {
          active: '[CampaignResults]',
          tabs: [
          {
            id: '[CampaignResults]',
            text: 'Results',
            icon: 'fa fa-list',
            boundControl: {
            dataset: '[CampaignResults]',
            options: { type: 'grid' }
            }
          },
          {               
            id: 'tab-overview',
            text: 'Overview',
            icon: 'fa fa-info-circle',
            boundControl: {
            dataset: 'CampaignResultsPivot',
            options: { type: 'grid', readOnly: true }
            }

          },
          {               
            id: 'tab-insights',
            text: 'Insights',
            icon: 'fa fa-bar-chart',               
            boundControl: {
            dataset: 'CampaignResultsPivot',
            toolbar: false,
            options: { type: 'chart', readOnly: true, 
              chartOptions: {                  
              type: 'bar',
              transpose: false,
              stack: false,
              showLegend: true,
              labels: true,
              xField: 0,
              xAxisName: 'Campaign',
              dimensionName: 'Name',
              ySeries: [1, 2, 3],
              
              seriesColors: {
                yes: '#17751aff',
                maybe: '#aa4400ff',
                no: '#6e110aff',                  
              }
              }
            }
            }
          }
          ]
        }
        }
      ]
      }
    }
    ]
  }
},

7.7.10 Bootstrapping

TODO

7.7.11 Smoke Tests

// Fire a single email
await Campaign.RSVP_Request({
    Mailer: 'RSVP',
    Campaign: 'Opening Night 2025-09-05',
    TemplateEmail: 'RSVP_TemplateEmail',
    TemplateThankYou: 'RSVP_TemplateThankYou',
    TemplateError: 'RSVP_TemplateError',
    Tracker: 'ReservationID',
    Subject: 'Can you make it?',
    MessageEmail: 'We’d love to see you!',
    MessageThankYou: 'Thanks for responding!',
    ErrorMessage: 'This invite is no longer valid.',
    email: 'customer@example.com'
  });

7.7.12 Production notes & gotchas

That’s it

You now have a replicable, full-stack example: DB → models → emails → endpoint → Backstage UI with pivot & chart. Paste the code as shown, wire the single endpoint, and you’ve got a working RSVP pipeline that your team can extend safely.

8. Part VIII – Monitoring & Analytics

Contents

8.1 Monitoring Framework

8.2 Security & Audit Compliance

8.3 Users, Assets & Role Rights

8.4 Network Whitelist

💡 Why It Matters The whitelist doesn’t just protect internal staff — it also governs external actors. By supporting masks and subnets, ASP.js lets admins safely admit essential crawlers (Facebook, Instagram, Google) without opening the floodgates. This provides measurable, auditable bot access and ensures security and marketing can coexist in the same operational framework.

8.5 Reporting Engine

💡 Why It Matters ASP.js doesn’t bolt on monitoring, auditing, or analytics — they are part of the schema contract. Sessions, user agents, exceptions, audits, bans, rights, whitelists, and reports are all first-class tables. This makes DevOps and compliance dramatically cheaper: dashboards, audits, and BI reports are just SQL away. By elevating these concerns to the database, ASP.js ensures observability, governance, and intelligence are defaults, not afterthoughts.


9. Part IX – Developer Experience

Contents

9.1 Developer Tools

9.2 Extensibility

9.3 Deployment Models

💡 Why It Matters Developer experience isn’t just about APIs — it’s about flow. With ASP.js and Backstage you get a debugger that spans client and server, a metadata contract that renders complete screens, extension points that don’t require glue code, and deployment models that fit everything from legacy IIS to Kubernetes. The result: developers spend less time wiring, more time shipping.


10. Part X – Conclusion

Contents

10.1 Key Takeaways

10.2 Future Directions

💡 Why It Matters ASP.js began as an emulation of Classic ASP but has evolved into a full-stack application framework where runtime, data, models, utilities, and UI converge. It modernizes familiar patterns without losing their simplicity, while adding the rigor, security, and debugging depth demanded by today’s applications. The journey ahead is about making this ecosystem even more expressive, visual, and intelligent, so developers can deliver robust business software at unprecedented speed.

11. Licensing

Access to the source code of the ASP.js VSCode Extension, ASP.js Runtime Framework and SQLX Native Data Access requires obtaining a Source Code License, enabling advanced customization and full transparency. For more information, contact info@mobilefx.com.

12. Publisher Information