Node.js 25 TypeScript, Built-in SQLite & Permission Model Guide
Guide · Updated June 2026

Node.js 25 TypeScript, SQLite & Permission Model:
Complete Production Guide

Native TypeScript support, built-in SQLite, and a granular permission model — all in Node.js 25+. This guide covers everything you need to know about these three game-changing features: from running .ts files directly without tsc to zero-config database storage with node:sqlite and hardening applications with --permission.

Oleg Maximov June 13, 2026 14 min read

1. Overview & Version Timeline

Node.js 25+ brings three transformative native features to the JavaScript runtime: native TypeScript support via built-in type stripping, built-in SQLite through the node:sqlite module, and the Permission Model for granular runtime security. This guide covers all three features in depth — from enabling Node.js TypeScript native execution without external dependencies, to using node:sqlite for zero-config database storage, to hardening applications with --permission and process.permission.has().

Version Release Date TypeScript SQLite Permission Model
v20 Apr 2024 Experimental (--experimental-permission)
v22 Apr 2024 Experimental type stripping node:sqlite added (v22.5.0) Experimental
v23 Oct 2024 Refinements, unsupported .tsx Experimental
v24 May 2025 Feature complete Feature complete --permission flag (removed experimental-)
v25 Oct 2025 Stable (Stability: 2), V8 14.1 Stable --allow-net added
v26 May 2026 Stable, documented as "Modules: TypeScript" Stable Stable, config file support

Key takeaway: As of Node.js 25+ (Current) and Node.js 24+ (LTS), all three features are stable and production-ready.

2. Native TypeScript Support (Type Stripping)

2.1 Enabling TypeScript in Node.js

There are two ways to run TypeScript code with Node.js:

Approach Description Best for
Built-in type stripping (recommended) Node.js strips TypeScript syntax at runtime, keeping only JavaScript. Zero config, no external dependencies. Simple TypeScript projects, scripts, prototypes
Third-party full support (e.g., tsx) Full TypeScript support including enums, namespaces, decorators, tsconfig.json features Complex TypeScript projects with advanced features

Built-in type stripping works by default — just run:

# Run a TypeScript file directly (Node.js 22+)
node app.ts

# No flags needed — type stripping is on by default in v22+

To disable type stripping:

node --no-strip-types app.ts

2.2 How Type Stripping Works

Node.js executes TypeScript files by replacing TypeScript-only syntax with whitespace (preserving line/column numbers in stack traces). No type checking is performed — that's still the job of tsc in your editor/CI.

Key principles:

// This code runs natively in Node.js:
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// The `: string` type annotations are stripped to whitespace
// Node.js sees: function greet(name) { return `Hello, ${name}!`; }

2.3 Supported File Extensions & Module System

Extension Module System Analogous JS Extension
.tsDetermined by nearest package.json.js
.mtsAlways ES module.mjs
.ctsAlways CommonJS.cjs
.tsxUnsupported

Module system rules for .ts files:

// Correct:
import './helper.ts';
import { config } from './config.ts';

// Wrong — will fail:
import './helper';
import { config } from './config';

// Also correct for CommonJS:
const helper = require('./helper.ts');

2.4 TypeScript Features That Work / Don't Work

FeatureSupported?Notes
Type annotations (: string)✅ YesStripped to whitespace
Interfaces✅ YesStripped
Type aliases✅ YesStripped
Generics✅ YesStripped
typeof, keyof, conditional types✅ YesPurely type-level
namespace (type-only)✅ YesOnly if no runtime code inside
const assertions✅ YesStripped
as casts✅ YesStripped
Enums❌ NoERR_UNSUPPORTED_TYPESCRIPT_SYNTAX
Namespaces with runtime code❌ Noe.g., namespace A { export let x = 1 }
Parameter properties❌ Noconstructor(private x: number)
Decorators❌ NoStill Stage 3 TC39 proposal
tsconfig.json paths❌ NoUse subpath imports (#) instead

Error example when using unsupported syntax:

TypeError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript features that require
JavaScript code generation are not supported. This file contains enum declarations
which require JavaScript code generation.

2.5 Importing Types (The type Keyword)

Due to how type stripping works, the type keyword is required in imports to distinguish type-only imports from value imports:

// ✅ Correct — will work:
import type { User, Config } from './types.ts';
import { createUser, type UserInput } from './user.ts';

// ❌ Wrong — will throw runtime error:
import { User, Config } from './types.ts'; // User is a type, not a value
import { createUser, UserInput } from './user.ts'; // UserInput is a type

Enable verbatimModuleSyntax in your tsconfig.json to enforce this behavior during development.

2.6 Recommended tsconfig.json Settings

For projects targeting Node.js 25+ with built-in type stripping:

{
  "compilerOptions": {
    "noEmit": true,
    "target": "esnext",
    "module": "nodenext",
    "rewriteRelativeImportExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true
  }
}
SettingPurpose
noEmit: trueDon't output .js files — Node.js runs .ts directly
target: "esnext"Don't downlevel modern JS features
module: "nodenext"Use Node.js's module resolution
rewriteRelativeImportExtensions: trueAllows writing .js in imports but resolving .ts
erasableSyntaxOnly: trueEnforce only syntax Node.js can strip
verbatimModuleSyntax: trueRequire type keyword in type imports

2.7 Full TypeScript Support via Third-Party

For projects that need enums, decorators, or other unsupported features, use a third-party loader like tsx:

# Install as a dev dependency
npm install --save-dev tsx

# Run directly
npx tsx your-file.ts

# Or use with node --import
node --import=tsx your-file.ts

Other similar libraries: ts-node, ts-blank-space, sucrase.

2.8 Type Stripping in Dependencies & Non-File Inputs

Dependencies under node_modules: Node.js refuses to handle TypeScript files inside node_modules. This prevents package authors from shipping TypeScript instead of compiled JavaScript.

Non-file inputs:

Path aliases: tsconfig.json "paths" are not supported. Use subpath imports instead:

// package.json
{
  "imports": {
    "#utils/*": "./src/utils/*.ts"
  }
}

// Usage:
import { formatDate } from '#utils/date.ts';

3. Built-in SQLite (node:sqlite)

3.1 Overview

import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(':memory:');
db.exec(`
  CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE
  );
`);

3.2 DatabaseSync Class

Constructor

new DatabaseSync(filename[, options])
ParameterTypeDefaultDescription
filenamestring | Buffer | URLPath to database file or ':memory:' for in-memory
options.openModenumberREADWRITE | CREATEBitwise OR of SQLITE_OPEN_* constants
options.readonlybooleanfalseOpen in read-only mode
options.fileMustExistbooleanfalseThrow if file doesn't exist
options.createbooleantrueCreate file if not exists
options.bigIntbooleanfalseReturn bigint for SQLite INTEGER columns

Methods

MethodDescription
db.prepare(sql)Prepare a SQL statement → returns StatementSync
db.exec(sql)Execute one or more SQL statements (DDL/batch)
db.export()Export entire database as Uint8Array
db.backup(targetFilename, options)Create a backup with optional progress callback
db.close()Close the database connection
db.serialize(fn)Serialize multiple database operations
db.transaction(fn)Create a transaction function
db.aggregate(name, options)Register a custom aggregate function
db.loadExtension(path, entryPoint?)Load a SQLite extension
db.enableDefensive(active)Enable defensive mode

3.3 StatementSync Class

Prepared statements created via db.prepare().

MethodDescriptionReturn Type
stmt.run(...values)Execute with values, returns {lastInsertRowid, changes}object
stmt.get(...values)Return first matching rowobject | undefined
stmt.all(...values)Return all matching rowsarray
stmt.iterate(...values)Return an iterator over result setIterable<object>

Usage Examples

// INSERT
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
const result = insert.run('Alice', '[email protected]');
console.log(result.lastInsertRowid); // 1

// SELECT single row
const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
const user = stmt.get(1);
console.log(user); // { id: 1, name: 'Alice', email: '[email protected]' }

// SELECT all rows
const allUsers = db.prepare('SELECT * FROM users').all();
console.log(allUsers); // [{ id: 1, ... }, { id: 2, ... }]

// Named parameters (using $, @, or : prefix)
const byEmail = db.prepare('SELECT * FROM users WHERE email = $email');
const user = byEmail.get({ $email: '[email protected]' });

3.4 Type Conversion Between JavaScript and SQLite

JavaScript TypeSQLite Type
stringTEXT
numberREAL or INTEGER
bigintINTEGER
booleanINTEGER (0 or 1)
Uint8ArrayBLOB
BufferBLOB
nullNULL
DateTEXT (ISO 8601 string)

3.5 Transactions & Serialization

// Transaction (atomic, auto-rollback on error)
const insertUser = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');

const insertUsers = db.transaction((users) => {
  for (const user of users) {
    insertUser.run(user.name, user.email);
  }
});

insertUsers([
  { name: 'Charlie', email: '[email protected]' },
  { name: 'Diana', email: '[email protected]' },
]);

// Serialization (ensure sequential access)
db.serialize(() => {
  db.exec('INSERT INTO users VALUES (1, "Alice")');
  db.exec('INSERT INTO users VALUES (2, "Bob")');
});

3.6 Backup & Export

// Backup with progress
const db = new DatabaseSync('source.db');
db.backup('backup.db', {
  progress: (remaining, total) => {
    const pct = ((total - remaining) / total * 100).toFixed(2);
    console.log(`Backup: ${pct}%`);
  }
});

// Export entire database as Uint8Array
const data = db.export();
// data is a Uint8Array — write to file, send over network, etc.

3.7 Complete Code Example

import { DatabaseSync } from 'node:sqlite';

// 1. Create/open database
const db = new DatabaseSync(':memory:');

// 2. Create schema
db.exec(`
  CREATE TABLE projects (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now'))
  );

  CREATE TABLE tasks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id INTEGER REFERENCES projects(id),
    title TEXT NOT NULL,
    done INTEGER DEFAULT 0
  );
`);

// 3. Insert with prepared statement
const insertProject = db.prepare('INSERT INTO projects (name) VALUES (?)');
const insertTask = db.prepare('INSERT INTO tasks (project_id, title) VALUES (?, ?)');

const projectResult = insertProject.run('My Project');
const projectId = projectResult.lastInsertRowid;

const addTasks = db.transaction(() => {
  insertTask.run(projectId, 'Design database schema');
  insertTask.run(projectId, 'Write API endpoints');
  insertTask.run(projectId, 'Test the application');
});
addTasks();

// 4. Query with joins
const tasks = db.prepare(`
  SELECT t.id, t.title, t.done, p.name as project
  FROM tasks t
  JOIN projects p ON t.project_id = p.id
  WHERE p.id = ?
`).all(projectId);

console.log(tasks);

// 5. Close
db.close();

4. Permission Model

4.1 Overview & History

The Node.js Permission Model is a runtime security mechanism that restricts what system resources a Node.js process can access.

VersionMilestone
v20.0.0Experimental with --experimental-permission
v24.0.0Flag renamed to --permission, increased stability
v25.0.0Added --allow-net
v26.3.0Stable, configuration file support, process.permission.drop() API

Important caveat: The Permission Model implements a "least base" approach. It prevents trusted code from unintentionally accessing restricted resources, but does NOT guarantee security in the presence of malicious code. Malicious code can potentially bypass the model.

4.2 Enabling the Permission Model

# Enable the permission model (restricts ALL permissions by default)
node --permission app.js

# Error example when accessing a restricted API:
# Error: Access to this API has been restricted
#   code: 'ERR_ACCESS_DENIED',
#   permission: 'FileSystemRead',
#   resource: '/home/user/index.js'

When --permission is enabled, the following are restricted by default:

4.3 Runtime API: process.permission

process.permission.has(scope[, reference]) — Check if a permission is granted:

// Check if file system write is allowed
process.permission.has('fs.write');   // true | false

// Check if specific file is writable
process.permission.has('fs.write', '/home/refs/protected-folder');
// true | false

// Check file system read
process.permission.has('fs.read');    // true | false
process.permission.has('fs.read', '/home/refs/protected-folder');

process.permission.drop(scope[, reference])Irreversibly drop a permission at runtime:

import fs from 'node:fs';

// Read config at startup while we still have permission
const config = fs.readFileSync('/etc/myapp/config.json', 'utf8');

// Drop read access to /etc/myapp after initialization
process.permission.drop('fs.read', '/etc/myapp');

// This will now throw ERR_ACCESS_DENIED
process.permission.has('fs.read', '/etc/myapp/config.json');
// → false

// Drop child process permission entirely (no reference = whole scope)
process.permission.drop('child');

4.4 File System Permissions

# Allow all file system operations
node --permission --allow-fs-read=* --allow-fs-write=* index.js

# Allow specific paths
node --permission --allow-fs-read=/tmp/ --allow-fs-write=/tmp/ index.js

# Multiple paths
node --permission \
  --allow-fs-read=/tmp/ \
  --allow-fs-read=/home/user/.gitignore \
  index.js

4.5 Network, Child Process, Worker, Addon Permissions

FlagSinceDescription
--allow-netv25.0.0Allow network access
--allow-child-processv20.0.0Allow spawning child processes
--allow-workerv20.0.0Allow creating worker threads
--allow-addonsv20.0.0Allow loading native addons
--allow-wasiv20.0.0Allow WASI access
--allow-ffiv20.0.0Allow FFI access
node --permission \
  --allow-net \
  --allow-fs-read=/app \
  --allow-fs-write=/app/data \
  server.js

4.6 Configuration File Support

Permissions can be declared in a node.config.json file (requires --experimental-default-config-file):

{
  "permission": {
    "--allow-fs-read": ["/foo"],
    "--allow-fs-write": ["/foo"],
    "--allow-child-process": true,
    "--allow-worker": true,
    "--allow-net": true,
    "--allow-addons": false,
    "--allow-ffi": false
  }
}

4.7 Constraints & Known Issues

Limitations:

  1. Permission Model is initialized after environment setup. Flags like --env-file or --openssl-config are evaluated before initialization and are NOT subjected to the model.
  2. OpenSSL engines cannot be queried when Permission Model is enabled, affecting crypto, https, and related modules.
  3. Explicit file descriptors via libuv bypass the permission model.
  4. Symbolic links are followed even to locations outside the granted paths.
  5. process._debugProcess() sends OS-level signals (SIGUSR1) to external processes and is NOT restricted by the Permission Model.

5. Migration Guide for Existing TypeScript Projects

5.1 Migration Strategy: Two Paths

ScenarioRecommended Approach
Simple app with only type annotations, interfaces, genericsBuilt-in type stripping — remove tsc build step
Complex app with enums, decorators, custom paths, or JSXKeep tsx or a third-party loader

5.2 Checklist for Type Stripping Migration

Step 1: Audit your TypeScript features — check for non-erasable syntax (enums, namespaces with runtime code, parameter properties, decorators).

Step 2: Update tsconfig.json with erasableSyntaxOnly and verbatimModuleSyntax. Run tsc --noEmit to verify compatibility.

Step 3: Fix import statements — add the type keyword where needed:

// Before:
import { User, Config, createUser } from './users.ts';

// After:
import type { User, Config } from './users.ts';
import { createUser } from './users.ts';

Step 4: Add file extensions to all imports:

// Before:
import { helper } from './utils/helper';

// After:
import { helper } from './utils/helper.ts';

Step 5: Replace tsconfig.json paths with subpath imports in package.json.

Step 6: Update package.json scripts — change "start": "node dist/index.js" to "start": "node src/index.ts".

5.3 Common Pitfalls

PitfallSolution
"Module not found" for type-only importsAdd type keyword: import type { ... }
"Cannot find module" without extensionAlways include .ts extension in imports
Enums not workingReplace with const union types or string literal unions
Decorators not supportedUse wrapper functions or wait for TC39 native decorators
tsconfig.json paths not workingMigrate to subpath imports (package.json imports field)
.tsx files not supportedUse .ts files or a third-party loader for JSX

6. FAQ

Is Node.js native TypeScript support production-ready?
Yes. Native TypeScript support via type stripping has been stable since Node.js 25 (Stability: 2). As of Node.js 26, it's documented as "Modules: TypeScript" and is fully supported in the Current and Active LTS release lines. All erasable TypeScript syntax — type annotations, interfaces, generics, type aliases — runs without flags or third-party tools.
Can I use node:sqlite in production?
Absolutely. The built-in SQLite module (node:sqlite) is stable in Node.js 25+ and Active LTS. It provides synchronous APIs (DatabaseSync, StatementSync) that are simpler and faster than async wrappers like better-sqlite3 for most use cases. It supports full SQL, transactions, WAL mode, and backup/export — all without installing a single npm package.
What's the difference between node:sqlite and better-sqlite3?
node:sqlite is a zero-dependency native module built directly into the Node.js runtime — no npm install needed, no native compilation, and it stays in sync with the Node.js release cycle. better-sqlite3 is an external npm package with a very similar synchronous API but requires native addon compilation and manual updates. For new projects, node:sqlite is the recommended choice.
Does the Permission Model protect against malicious code?
The Node.js Permission Model follows a "seat belt" approach — it prevents trusted code from unintentionally accessing resources outside its granted scope. It is not designed to protect against malicious code that deliberately exploits the runtime. For security-critical applications, combine the Permission Model with OS-level sandboxing (containers, seccomp, AppArmor).
How do I enable all three features together?
Run Node.js with the --permission flag and grant specific permissions: node --permission --allow-fs-read=/data --allow-fs-write=/data app.ts. This enables Node.js TypeScript native execution (no flags needed for type stripping), gives your app database access via node:sqlite, and restricts the process to only the file paths you specify.
What TypeScript features don't work with native type stripping?
Enums, namespaces with runtime code, parameter properties (constructor(private x)), decorators, and tsconfig.json path resolution. These require third-party tools like tsx or the TypeScript compiler. See section 2.4 for the full compatibility table.
Do I need to change my imports for Node.js native TypeScript?
Yes — file extensions are mandatory in all imports when running TypeScript natively. Always include the .ts extension: import { config } from './config.ts' (correct) vs import { config } from './config' (fails). See section 2.3 for details.
How does the Permission Model work with node:sqlite?
The Permission Model can restrict file system access to the SQLite database files. Grant read/write access to specific paths where your database files live: node --permission --allow-fs-read=/data --allow-fs-write=/data app.ts. Without these permissions, node:sqlite operations that read or write database files will fail with ERR_ACCESS_DENIED. See section 4 for the full Permission Model API.

7. Reference & Further Reading

Related Articles on maximov.by:

Official Documentation:

Need to build a modern Node.js application with TypeScript, SQLite, and proper security? I'm a full-stack developer with deep experience in Node.js, TypeScript, and production architecture. Let's discuss your project.

Contact

Let's discuss your project

Tell me about your project — I'll provide expert advice on architecture, technology choices, and a preliminary estimate. Free of charge.