Blogs/Technology

How To Use Typesense with Node.js

Written byIshwarya
Aug 6, 2026
14 Min Read
How To Use Typesense with Node.js Hero
Too Long? Read This First
- Typesense is an open-source search engine built for fast, typo-tolerant search.
- Your primary database should remain the source of truth; Typesense stores a searchable copy of the required data.
- A collection is similar to a database table, while documents are similar to rows.
- The official JavaScript client can be installed using npm install typesense.
- Use a write-enabled API key only on the backend.
- Use a search-only or scoped key for requests made from browsers and mobile applications.
- Fields must be declared correctly in the collection schema before they can be searched, filtered, faceted, or sorted.
- Keep Typesense synchronised whenever records are created, updated, or deleted in the primary database.

Search looks simple until real users begin typing incomplete product names, spelling words incorrectly, combining filters, and expecting useful results instantly. A basic database query may work initially, but it becomes difficult to manage once the application needs typo tolerance, relevance ranking, faceting, and search-as-you-type.

That is what led me to explore Typesense for Node.js applications. It provides the search capabilities users expect without requiring every behaviour to be built from scratch. In this guide, I’ll show you how to run Typesense, connect it to Node.js, create a collection, index documents, and implement practical searches, filters, facets, sorting, and pagination.

What Is Typesense?

Typesense is an open-source search engine designed for fast, relevant, and typo-tolerant search. Applications send searchable documents to Typesense and query them through its API.

It can be self-hosted or used through Typesense Cloud. Typesense is commonly added to e-commerce stores, directories, documentation websites, content platforms, and applications that need search-as-you-type experiences.

How Does Typesense Work With Node.js?

Typesense does not normally replace your application database. PostgreSQL, MySQL, MongoDB, or another database should continue to store the authoritative version of your data.

The Node.js application sends selected fields from that data to Typesense. These fields are indexed so that Typesense can search them efficiently.

A typical request flow looks like this:

  1. A product is created or updated in the primary database.
  2. The Node.js backend sends the searchable product fields to Typesense.
  3. A user enters a query in the application.
  4. The application sends the query to Typesense.
  5. Typesense searches its index and returns matching document IDs and fields.
  6. The application displays the results or retrieves additional information from the primary database.

This separation matters. Typesense is responsible for discovery and search relevance, while the database remains responsible for durable application data and business transactions.

Why Use Typesense?

Typesense can return relevant results even when a query contains minor spelling mistakes. A search for hedphones, for example, can still find products containing headphones.

2. Fast Search Experiences

The engine is designed for low-latency searches, making it suitable for autocomplete and search-as-you-type interfaces.

3. Filtering and Faceting

Search results can be filtered by fields such as price, category, availability, brand, or location. Facets can also show users how many results belong to each category.

4. Relevance Control

Developers can control which fields are searched, assign different importance to them, sort results, and fine-tune typo tolerance and matching behaviour.

5. Deployment Flexibility

Typesense can be self-hosted using Docker, installed as a binary, or run through Typesense Cloud.

6. Simpler Search Setup

Compared with broader search and analytics platforms, Typesense focuses on application search and provides a relatively direct API and schema model.

Common Typesense Use Cases

Typesense can support several search-heavy application experiences.

Users can search product names and descriptions while filtering by category, brand, price, rating, colour, or availability.

Knowledge bases and documentation websites can index page titles, headings, body content, tags, and categories.

Directories can combine keyword search with location, category, price, rating, or availability filters.

Media and Content Discovery

Applications can index articles, videos, podcasts, tags, descriptions, and transcripts to make large content libraries easier to explore.

Typesense supports geographical search for applications that need to find nearby stores, restaurants, service providers, or delivery locations.

How to Integrate Typesense With Node.js

We will build a simple product-search example. The collection will contain a product title, description, category, price, stock status, and popularity score.

Step 1: Run Typesense Locally

The easiest local setup is through Docker.

Create a directory for persistent Typesense data:

mkdir typesense-data

Start a Typesense server:

docker run -d \
  --name typesense \
  -p 8108:8108 \
  -v "$(pwd)/typesense-data:/data" \
  typesense/typesense:30.2 \
  --data-dir /data \
  --api-key=YOUR_ADMIN_API_KEY \
  --enable-cors

Replace YOUR_ADMIN_API_KEY with a strong secret value.

Typesense will now be available at:

http://localhost:8108

For production environments, use a supported stable version, persistent storage, backups, monitoring, and high-availability configuration appropriate to your application.

If you do not want to manage the infrastructure, you can create a hosted cluster through Typesense Cloud.

Step 2: Create a Node.js Project

Create a new project and install the required packages:

mkdir typesense-node-demo
cd typesense-node-demo
npm init -y
npm install typesense dotenv

Create a .env file:

TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http
TYPESENSE_ADMIN_API_KEY=YOUR_ADMIN_API_KEY

Do not commit this file to version control. Add it to .gitignore:

node_modules
.env

Step 3: Initialise the Typesense Client

Create a file named typesenseClient.js:

require("dotenv").config();

const Typesense = require("typesense");

const client = new Typesense.Client({
  nodes: [
    {
      host: process.env.TYPESENSE_HOST,
      port: Number(process.env.TYPESENSE_PORT),
      protocol: process.env.TYPESENSE_PROTOCOL,
    },
  ],
  apiKey: process.env.TYPESENSE_ADMIN_API_KEY,
  connectionTimeoutSeconds: 2,
});

module.exports = client;

The configuration contains:

  • host: The Typesense server or cloud-cluster hostname
  • port: The port on which Typesense is available
  • protocol: http locally or https for most hosted environments
  • apiKey: The key used to authorise requests
  • connectionTimeoutSeconds: How long the client waits before treating the connection as unavailable

A production configuration can include multiple nodes and a nearest node to improve availability and request routing.

Step 4: Check the Connection

Create checkConnection.js:

const client = require("./typesenseClient");

async function checkConnection() {
  try {
    const collections = await client.collections().retrieve();
    console.log("Connected to Typesense");
    console.log(collections);
  } catch (error) {
    console.error("Typesense connection failed:", error);
  }
}

checkConnection();

Run it:

node checkConnection.js

If the connection succeeds, Typesense returns the available collections. A new instance will normally return an empty list.

If it fails, verify:

  • The Typesense container is running.
  • The host and port are correct.
  • The API key matches the server key.
  • The protocol is correct.
  • Port 8108 is accessible.

Step 5: Create a Collection

A collection contains related documents and defines how their fields should be indexed.

Create createCollection.js:

const client = require("./typesenseClient");

const productsSchema = {
  name: "products",
  fields: [
    {
      name: "title",
      type: "string",
    },
    {
      name: "description",
      type: "string",
      optional: true,
    },
    {
      name: "category",
      type: "string",
      facet: true,
    },
    {
      name: "price",
      type: "float",
      facet: true,
    },
    {
      name: "in_stock",
      type: "bool",
      facet: true,
    },
    {
      name: "popularity",
      type: "int32",
    },
  ],
  default_sorting_field: "popularity",
};

async function createCollection() {
  try {
    const collection = await client.collections().create(productsSchema);
    console.log("Collection created:", collection);
  } catch (error) {
    console.error("Unable to create collection:", error);
  }
}

createCollection();

Run the file:

node createCollection.js

The schema tells Typesense how each field will be used:

  • string is suitable for searchable text.
  • float stores prices or decimal values.
  • bool stores true or false values.
  • int32 stores whole numbers.
  • optional: true allows documents to omit that field.
  • facet: true allows the field to be used for faceting and efficient filtering.
  • default_sorting_field helps rank equally relevant results using the popularity score.

Let’s Build Scalable Node.js Apps Together!

We build secure, high-performance Node.js backends that handle heavy traffic and scale with your business.

Typesense also supports automatic schema detection, but an explicit schema gives you clearer control in production applications.

Step 6: Retrieve, Update, and Delete Collections

Retrieve All Collections

const client = require("./typesenseClient");

async function getCollections() {
  const collections = await client.collections().retrieve();
  console.log(collections);
}

getCollections();

Retrieve One Collection

const collection = await client.collections("products").retrieve();
console.log(collection);

Add a Field to an Existing Collection

Typesense supports adding and removing fields through a schema update.

const updatedSchema = await client.collections("products").update({
  fields: [
    {
      name: "brand",
      type: "string",
      facet: true,
      optional: true,
    },
  ],
});

console.log(updatedSchema);

To remove an existing field, mark it for deletion:

await client.collections("products").update({
  fields: [
    {
      name: "brand",
      drop: true,
    },
  ],
});

Changing the type or configuration of an existing field may require removing and recreating it. For larger production collections, plan schema migrations carefully.

Delete a Collection

const deletedCollection = await client
  .collections("products")
  .delete();

console.log(deletedCollection);

Deleting a collection removes its indexed documents. Use this operation carefully, especially in shared or production environments.

Step 7: Add Documents

A document is an individual searchable record inside a collection.

Add One Document

const client = require("./typesenseClient");

const product = {
  id: "1",
  title: "Wireless Laptop Mouse",
  description: "Compact Bluetooth mouse for laptops and tablets",
  category: "Accessories",
  price: 29.99,
  in_stock: true,
  popularity: 85,
};

async function addProduct() {
  const result = await client
    .collections("products")
    .documents()
    .create(product);

  console.log(result);
}

addProduct();

Typesense accepts the id as a string. If it is omitted, Typesense can generate one automatically, but using your database record ID makes synchronisation easier.

Import Multiple Documents

const client = require("./typesenseClient");

const products = [
  {
    id: "1",
    title: "Wireless Laptop Mouse",
    description: "Compact Bluetooth mouse for laptops and tablets",
    category: "Accessories",
    price: 29.99,
    in_stock: true,
    popularity: 85,
  },
  {
    id: "2",
    title: "Gaming Laptop",
    description: "High-performance laptop with dedicated graphics",
    category: "Computers",
    price: 1299.99,
    in_stock: true,
    popularity: 98,
  },
  {
    id: "3",
    title: "USB-C Phone Charger",
    description: "Fast charger for phones and tablets",
    category: "Accessories",
    price: 19.99,
    in_stock: false,
    popularity: 72,
  },
];

async function importProducts() {
  const result = await client
    .collections("products")
    .documents()
    .import(products, {
      action: "upsert",
    });

  console.log(result);
}

importProducts();

The upsert action creates a document when its ID does not exist and updates it when it does. This is useful when synchronising records from a database.

Bulk imports return an individual result for each document. Check the responses instead of assuming every record succeeded.

Step 8: Retrieve, Update, Upsert, and Delete Documents

Retrieve a Document

const product = await client
  .collections("products")
  .documents("1")
  .retrieve();

console.log(product);

Update a Document

const updatedProduct = await client
  .collections("products")
  .documents("1")
  .update({
    price: 24.99,
    in_stock: false,
  });

console.log(updatedProduct);

The update operation changes only the supplied fields.

Upsert a Document

const product = await client
  .collections("products")
  .documents()
  .upsert({
    id: "1",
    title: "Wireless Laptop Mouse",
    description: "Compact Bluetooth mouse with silent buttons",
    category: "Accessories",
    price: 24.99,
    in_stock: true,
    popularity: 90,
  });

console.log(product);

Delete a Document

const deletedProduct = await client
  .collections("products")
  .documents("1")
  .delete();

console.log(deletedProduct);

When a record is deleted from the primary database, remove its corresponding Typesense document as well.

How to Search Documents in Typesense?

Perform a Simple Search

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "laptop",
    query_by: "title,description",
  });

console.log(results.hits);

The two important parameters are:

  • q: The text entered by the user
  • query_by: The fields Typesense should search

The order of fields in query_by matters. Here, a match in title is considered more relevant than a match in description.

query_by does not sort the response alphabetically. It tells Typesense which text fields should participate in the full-text search.

Search With Typo Tolerance

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "gamng latop",
    query_by: "title,description",
    num_typos: 2,
  });

console.log(results.hits);

Typesense supports typo tolerance by default. The num_typos parameter lets you control the number of permitted typographical errors.

Allowing more typos is not always better. Excessive tolerance can produce irrelevant matches and require more processing.

Search Multiple Words

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "wireless laptop",
    query_by: "title,description",
  });

console.log(results.hits);

Typesense searches the individual query tokens and ranks documents based on factors such as matching fields, token proximity, typo count, and text match quality.

A multi-word query does not necessarily guarantee that every word must appear in every result. If you require an exact phrase, surround it with quotation marks:

const results = await client
  .collections("products")
  .documents()
  .search({
    q: '"wireless laptop"',
    query_by: "title,description",
  });

Filtering Search Results

Filters narrow the result set using structured fields.

Filter by a Numeric Value

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "*",
    query_by: "title",
    filter_by: "price:>500",
  });

console.log(results.hits);

The wildcard query q: "*" retrieves documents without requiring a text match. The filter then keeps products priced above 500.

Filter by a Numeric Range

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "*",
    query_by: "title",
    filter_by: "price:>=20 && price:<=100",
  });

console.log(results.hits);

The && operator combines conditions that must both be true.

Filter by Category and Availability

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "mouse",
    query_by: "title,description",
    filter_by: "category:=Accessories && in_stock:=true",
  });

console.log(results.hits);

The := operator performs an exact filter match.

Filter by Multiple Values

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "*",
    query_by: "title",
    filter_by: "category:=[Accessories,Computers]",
  });

console.log(results.hits);

This returns products belonging to either selected category.

Filter by Document IDs

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "*",
    query_by: "title",
    filter_by: "id:=[1,2]",
  });

console.log(results.hits);

Exclude a Value

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "*",
    query_by: "title",
    filter_by: "id:!=1",
  });

console.log(results.hits);

This excludes the document with the ID 1.

Adding Facets

Facets group matching documents by a field and return the number of results in each group.

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "*",
    query_by: "title",
    facet_by: "category,in_stock",
  });

console.log(results.facet_counts);

A response might show:

  • Accessories: 2
  • Computers: 1
  • In stock: 2
  • Out of stock: 1

The frontend can display these values as filters.

Faceting does not automatically filter the results. It provides grouped counts. The user’s selection must then be sent through filter_by.

For example:

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "laptop",
    query_by: "title,description",
    facet_by: "category",
    filter_by: "category:=Computers",
  });

Sorting Results

Use sort_by when results must follow a particular order.

Sort by Price

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "*",
    query_by: "title",
    sort_by: "price:asc",
  });

console.log(results.hits);

Combine Relevance and Popularity

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "laptop",
    query_by: "title,description",
    sort_by: "_text_match:desc,popularity:desc",
  });

console.log(results.hits);

This keeps text relevance as the primary ranking factor and uses popularity to break ties or improve the order among similarly relevant results.

Paginating Search Results

Typesense supports page-based pagination:

const results = await client
  .collections("products")
  .documents()
  .search({
    q: "*",
    query_by: "title",
    page: 2,
    per_page: 10,
  });

console.log(results.hits);

The response also includes information such as the total number of matching documents and the processing time.

Do not request an unnecessarily large number of results in one response. Use pagination or an appropriate export workflow for larger datasets.

Putting Search Behind an Express API

Install Express:

npm install express

Create server.js:

const express = require("express");
const client = require("./typesenseClient");

const app = express();

app.use(express.json());

app.get("/api/search/products", async (request, response) => {
  try {
    const {
      q = "*",
      category,
      minPrice,
      maxPrice,
      page = 1,
    } = request.query;

    const filters = [];

    if (category) {
      filters.push(`category:=${category}`);
    }

    if (minPrice) {
      filters.push(`price:>=${Number(minPrice)}`);
    }

    if (maxPrice) {
      filters.push(`price:<=${Number(maxPrice)}`);
    }

    const searchParameters = {
      q,
      query_by: "title,description",
      facet_by: "category,in_stock",
      sort_by: "_text_match:desc,popularity:desc",
      page: Number(page),
      per_page: 10,
    };

    if (filters.length > 0) {
      searchParameters.filter_by = filters.join(" && ");
    }

    const results = await client
      .collections("products")
      .documents()
      .search(searchParameters);

    response.json(results);
  } catch (error) {
    console.error("Search failed:", error);
    response.status(500).json({
      message: "Unable to search products",
    });
  }
});

app.listen(3000, () => {
  console.log("Search API running at http://localhost:3000");
});

Run the server:

node server.js

Try a request:

http://localhost:3000/api/search/products?q=laptop&category=Computers&minPrice=500

In a real application, validate and escape all user-controlled filter values before constructing a Typesense filter expression. Do not pass unrestricted user input directly into filter_by or sort_by.

Let’s Build Scalable Node.js Apps Together!

We build secure, high-performance Node.js backends that handle heavy traffic and scale with your business.

How to Keep Typesense and Your Database in Sync

Typesense should reflect changes made to your primary database.

Update Both Within the Application

After a successful database write, add or update the corresponding Typesense document:

const savedProduct = await database.products.create(productData);

await client
  .collections("products")
  .documents()
  .upsert({
    id: String(savedProduct.id),
    title: savedProduct.title,
    description: savedProduct.description,
    category: savedProduct.category,
    price: savedProduct.price,
    in_stock: savedProduct.inStock,
    popularity: savedProduct.popularity,
  });

This is simple, but the database could succeed while the Typesense update fails.

Use a Queue or Background Worker

For more reliable production synchronisation:

  1. Save the data in the primary database.
  2. Publish an indexing job to a queue.
  3. Let a worker update Typesense.
  4. Retry failed jobs.
  5. Record indexing errors for investigation.

An outbox pattern can provide stronger guarantees by storing the data change and indexing event in the same database transaction.

Reindex Periodically

Even with event-driven updates, a scheduled reconciliation process can compare the database with Typesense and repair missing or outdated documents.

Securing Typesense in a Node.js Application

The bootstrap or admin API key can create collections, write documents, and manage other keys. Never expose it in browser code, mobile applications, public repositories, or client-visible environment variables.

Use separate keys for different responsibilities:

  • Backend write key for indexing documents
  • Search-only key for public search requests
  • Scoped search key for user- or tenant-specific access
  • Admin key for restricted operational tasks

A search-only key can be limited to the search action:

{
  "description": "Search products only",
  "actions": ["documents:search"],
  "collections": ["products"]
}

For a multi-tenant application, scoped search keys can enforce a filter such as:

tenant_id:=customer_42

The filter is embedded in the key and cannot be removed by the client. This is safer than trusting the frontend to send the correct tenant filter.

Teams building large Node.js search integrations may also need support with schema planning, background synchronisation, API security, and scaling. In such cases, experienced Node.js developers can help design the search layer around the application’s actual data and traffic patterns.

Common Typesense Errors and Solutions

Connection Refused

Cause: Typesense is not running, or the configured host and port are incorrect.

Fix: Check the container status, port mapping, protocol, and environment variables.

docker ps

HTTP 401 or 403 Error

Cause: The API key is incorrect or does not have permission to perform the requested action.

Fix: Confirm that the backend uses a key with the required collection and action permissions.

Field Not Found in Schema

Cause: A document or query refers to a field that is not defined in the collection.

Fix: Add the field through a schema update or correct the field name. Ensure the document structure matches the schema.

Document Does Not Match the Schema

Cause: A value uses the wrong type, such as sending a string for a field declared as float.

Fix: Transform the data before indexing and inspect individual bulk-import results for failures.

Filtering or Faceting Does Not Work

Cause: The field has not been configured appropriately, or the filter syntax is incorrect.

Fix: Mark fields used for faceting withfacet: true, use the correct operators, and validate the expression against the relevant Typesense version.

Search Returns Unexpected Results

Cause: The wrong fields are listed inquery_by, typo tolerance is too broad, or ranking does not match the product’s needs.

Fix: Review field order, query weights, typo settings, filtering, and sorting. Test using real user queries rather than only ideal keywords.

Typesense Production Checklist

Before launching, confirm that:

  • Typesense uses persistent storage.
  • The admin key is stored only in a secret manager or secure backend environment.
  • Browser requests use a search-only or scoped key.
  • The primary database remains the source of truth.
  • Indexing failures are retried and monitored.
  • Bulk-import responses are inspected.
  • Backups and recovery procedures have been tested.
  • Search latency and error rates are monitored.
  • High availability is configured when downtime would affect critical workflows.
  • Collections can be rebuilt from the primary database.
  • Search behaviour has been tested with real queries and spelling mistakes.
  • Schema changes and reindexing are included in the deployment plan.

Frequently Asked Questions

Is Typesense Suitable for Production Node.js Applications?

Yes. Typesense supports typo-tolerant search, real-time indexing, filters, facets, sorting, geographical search, and high-availability deployments. Production readiness still depends on secure configuration, persistent storage, monitoring, backups, and reliable synchronisation.

Does Typesense Replace My Database?

No. Your database should remain the source of truth for application data and transactions. Typesense stores a searchable representation of selected fields and is optimised for retrieval, relevance, filtering, and discovery.

Is Typesense an Alternative to Elasticsearch?

Typesense can be an alternative when the primary requirement is application search with a simpler setup. Elasticsearch offers a broader search and analytics ecosystem but may require more operational knowledge and configuration.

Can I Use Typesense Directly From the Frontend?

Yes, but only with a search-only or scoped API key. Never expose an admin or write-enabled key in browser or mobile code, since it could allow unauthorised data or collection changes.

Does Typesense Support Typo Tolerance Automatically?

Yes. Typo tolerance is enabled by default for searchable text. Developers can control it through parameters such as num_typos and other matching settings when the default behaviour is too broad or restrictive.

Does Typesense Support Filters and Facets?

Yes. Filters restrict the returned documents, while facets group results and provide counts for selected fields. Fields used for faceting should be configured with facet: true in the collection schema.

Is Typesense Good for Multi-Tenant Applications?

Yes, provided access is designed correctly. Scoped search keys can enforce tenant-specific filters, preventing users from removing or overriding the access restriction included in their search credentials.

Our Final Words

Typesense works best when search is treated as a product feature rather than a basic database query. The goal is not simply to return records containing a word. It is to help users find the right product, document, or page even when their query is incomplete or misspelled.

What stood out while working through the Node.js integration was how quickly the basic pieces came together: define a schema, index the searchable fields, and send queries through the JavaScript client. The more important engineering work begins afterwards, tuning relevance, protecting API keys, synchronising database changes, and testing with real search behaviour.

Start with a small collection and a representative set of user queries. Once the results are useful and consistent, expand the schema, introduce filters and facets, and build a reliable synchronisation process for production.

Author-Ishwarya
Ishwarya

Passionate Full Stack Developer with 3+ years of experience. Always eager to explore emerging technologies and push the boundaries of innovation in both tech and life. 🚀

Share this article

Phone

Next for you

8 Best GraphQL Libraries for Node.js in 2025 Cover

Technology

Aug 4, 202613 min read

8 Best GraphQL Libraries for Node.js in 2025

8 Best GraphQL Libraries for Node.js in 2026 Too Long? Read This First - Choose Apollo Server when you need a mature ecosystem, GraphOS integration, plugins, or Apollo Federation. - Choose GraphQL Yoga for a modern, portable server with Fetch API compatibility and built-in support for subscriptions over Server-Sent Events. - Choose Mercurius when your application already uses Fastify and runtime efficiency is a major priority. - Use GraphQL.js when you need the official JavaScript implementati

9 React Native Animation Libraries and Tools Compared Cover

Technology

Aug 4, 202615 min read

9 React Native Animation Libraries and Tools Compared

Too Long? Read This First - Use React Native Reanimated for gesture-driven, interruptible, and performance-sensitive interface animations. - Use the built-in Animated API for simple fades, transforms, and timed sequences without another dependency. - Pair React Native Gesture Handler with Reanimated for swipes, dragging, pinching, rotation, and other touch-driven experiences. - Use Lottie React Native for non-interactive motion graphics supplied by designers. - Choose React Native Skia for cust

9 Critical Practices for Secure Web Application Development Cover

Technology

Aug 4, 202616 min read

9 Critical Practices for Secure Web Application Development

Too Long? Read This First - Define security requirements and model threats before implementation begins. - Treat authentication, account recovery, and MFA as one complete identity system. - Apply server-side authorization to every protected action and object. - Prevent injection with parameterized APIs, structured validation, safe output handling, and restricted outbound requests. - Protect sessions and tokens throughout their complete lifecycle. - Minimise sensitive data and manage encryption