Docly Developers
Knowledge base Misconception By design

Docly JavaScript is not Node

In shortDocly's server-side JavaScript is HashJS, Docly's own engine written in .NET — not Node. There is no npm, no package.json, no node_modules, and no working require() or fetch(). But the missing names are declared and null rather than undefined, so typeof fetch !== "undefined" answers true and feature detection reaches the wrong conclusion. A bare process global exists as well — the HashJS evaluator, not Node's. Code comes from import of files under #/ or from Docly packages, and everything the platform offers lives on the docly.* object.
Applies to JavaScriptAPIHash templates
  1. What you'll see

    Code written for a Docly workspace opens with const fs = require("fs"), calls await fetch(url), reads process.env.API_KEY, or arrives with a package.json and an npm install step. An AI coding agent asked to add a dependency proposes installing a package.

    None of it works, and the failure is rarely a clean error — it is an undefined symbol part-way through a page render, so the page just stops producing output at that point.

  2. What's actually happening

    Docly's server-side JavaScript engine is HashJS. It is Docly's own engine, written in .NET: the language is JavaScript, but the runtime around it is Docly's, not Node's. Almost nothing from the Node standard library is present, and there is no package manager: the absence of package.json and node_modules in a Docly workspace is the normal state, not a missing setup step.

    The same applies in the browser direction — this is not browser JavaScript either. There is no window, no document, and no fetch on the server side.

    One name genuinely collides. docly.process() exists as a script evaluator — it executes a Docly script string with a supplied set of globals, and requires specific access because of the back-door risk. It has nothing to do with Node's process object. There is also a bare global process, which is that same evaluator rather than Node's: it is a function, and process.env is null, so process.env.API_KEY fails on a null property access instead of quietly reading undefined.

    That points at the wider trap. The absent names are declared and null, not undefined, which breaks the standard feature-detection idiom:

    typeof fetch                   // "object"  -- because fetch is null
    typeof fetch !== "undefined"   // true      -- so the detect passes
    fetch(url)                     // and then fails anyway

    Measured inventory, re-checked after the August 2026 engine update:

    Declared but nullPresent and usable
    require, fetch, window, document, module, exports, setTimeoutconsole, JSON, Math, Date, Promise, Buffer, crypto, btoa, atob, process

    btoa and atob work as expected — btoa("abc") returns "YWJj". Buffer and crypto resolve to objects rather than being absent, but presence is not a promise of Node semantics: check what a member actually does before relying on it. And setTimeout being null means there are no timers at all — there is nothing to defer work onto.

    What you get instead is a single platform library, docly.*, covering filesystem, HTTP, mail, images, authentication, sharing and output; plus import for your own code. The standard built-ins are there as usual: JSON, Math, String, Array, Date, console.log.

  3. What to do

    Reach for the Docly equivalent rather than the Node idiom:

    Instead ofUse
    require("fs"), fs.readFileFilesystem functions — docly.getFile, docly.getFiles, docly.getJson, docly.saveFile, docly.saveJson
    fetch, axios, node-fetchNetwork functions — docly.httpGet (JSON-parsed body), docly.httpPost, docly.httpFetch (full response: status, headers, raw body)
    nodemailerMail functions — smtp.send, plus POP3 and IMAP
    process.env.SECRETA JSON file under #/, read with docly.getJson(). The #/ tree is never served over HTTP — see Keep private data under the # folder
    npm install <pkg>A Docly package, or your own .js file under #/
    require("./helpers")import { helper } from "#/helpers.js" — absolute paths from site root, no relative ./. Named imports, namespace imports (import * as m from "…") and import("…") work; a default import (import x from "…") does not
    module.exportsexport default / named export
    Express req / resAuto-bound function parameters plus the query and form globals; the returned object is the response. See Reading query and form fields in API calls
    path.join, relative pathsAbsolute paths from site root. In anything the browser will execute, use ~/ — see Use tilde paths in JavaScript

    Most of these exist only in #/API/ scripts. The network functions (httpGet, httpPost, httpFetch …), the mail functions, the share functions and every function that writes are registered for API scripts only; in a .hash page the call fails with is not a function. Reading functions such as getFile, getFiles and getJson work everywhere. See Some docly functions exist only in API scripts.

    A minimal API endpoint, for orientation:

    // #/API/orders/list.js
    import { format } from "#/helpers.js";
    
    export default () => {
        let rows = docly.getFiles("/Orders", { recursive: true });
        return { count: rows.length, items: rows.map(format) };
    };

    Do not feature-detect with typeof. Because the absent names are null rather than undefined, typeof x !== "undefined" is true for every one of them. Test x === null if you must, or better, do not reach for the Node name at all.

    When you are unsure what a function returns, print it rather than assuming a Node-shaped result. A throwaway .hash page is the fastest way — see Use a scratch hash file to test Docly functions.

    Start from the JavaScript reference, and read About JavaScript and HASH files before writing much of anything.

Last reviewed 17 Oct 2026