Skip to main content

Mutating Documents with TypeScript Hooks

This example demonstrates how to use hooks in TypeScript to modify documents automatically when they're created or updated in your Juno Satellite.

Hooks let you react to events like document creation, deletion, or asset uploads — and run custom backend logic in response.

You can browse the source code here: github.com/junobuild/examples/tree/main/functions/typescript/hooks


Folder Structure

typescript/hooks/
├── src/
│ ├── satellite/ # TypeScript Satellite serverless function
│ │ ├── index.ts # Main TypeScript logic for Satellite
│ │ └── tsconfig.json # TypeScript config for Satellite
│ ├── declarations/
│ │ └── satellite/ # TypeScript declarations for Satellite
│ ├── admin.ts # Frontend admin logic
│ ├── doc.ts # Frontend doc logic
│ ├── main.ts # Frontend entry point
│ ├── storage.ts # Frontend storage logic
│ ├── style.css # Frontend styles
│ └── types.ts # Shared types and schemas
├── juno.config.ts # Juno Satellite configuration
├── package.json # Frontend dependencies
└── ...

Key Features

  • Serverless Hooks in TypeScript: Demonstrates how to react to data and asset operations using hooks in TypeScript serverless functions.
  • Multiple Hook Types: Includes hooks for document set operations (extendable for set-many, delete, upload, etc.).
  • Serverless Integration: Runs as a Satellite function and integrates with Juno's datastore and authentication system.
  • Minimal UI for Testing: A simple frontend is included to test and demonstrate the hook logic in action.

Main Backend Components

  • src/satellite/index.ts: The core TypeScript logic for the Satellite serverless function. Implements hooks for various operations (set, assert, etc.).
  • src/types.ts: Shared Zod schema and types for document validation and decoding.

Example: Mutating Documents

Here’s the actual TypeScript logic from index.ts:

import {
type AssertSetDoc,
defineAssert,
defineHook,
type OnSetDoc
} from "@junobuild/functions";
import { PersonData, PersonDataSchema } from "../types";
import {
decodeDocData,
encodeDocData,
setDocStore
} from "@junobuild/functions/sdk";
import { Principal } from "@dfinity/principal";

export const assertSetDoc = defineAssert<AssertSetDoc>({
collections: ["demo"],
assert: (context) => {
// We validate that the data submitted for create or update matches the expected schema.
const person = decodeDocData<PersonData>(context.data.data.proposed.data);
PersonDataSchema.parse(person);
}
});

export const onSetDoc = defineHook<OnSetDoc>({
collections: ["demo"],
run: async (context) => {
const {
caller,
data: {
key,
collection,
data: { after: currentDoc }
}
} = context;

// We decode the new data saved in the Datastore because it holds those as blob.
const person = decodeDocData<PersonData>(currentDoc.data);

// Some console.log for demo purpose
console.log(
"[on_set_doc] Caller:",
Principal.fromUint8Array(caller).toText()
);
console.log("[on_set_doc] Collection:", collection);
console.log("[on_set_doc] Data:", person.principal, person.value);

// We update the document's data that was saved in the Datastore with the call from the frontend dapp.
const { hello, ...rest } = person;
const updatePerson = {
...rest,
hello: `${hello} checked`,
yolo: false
};

// We encode the data back to blob.
const updateData = encodeDocData(updatePerson);

// We save the document for the same caller as the one who triggered the original on_set_doc, in the same collection with the same key as well.
setDocStore({
caller: caller,
collection,
key,
doc: {
version: currentDoc.version,
data: updateData
}
});
}
});

Explanation:

  • Defines a PersonData type and Zod schema for validation.
  • Uses defineAssert to validate document data before creation or update.
  • Uses defineHook to run logic whenever a document is set in the demo collection. Updates the document and saves it back.
  • Uses the Juno SDK for encoding/decoding and storing documents.

How to Run

  1. Clone the repo:
git clone https://github.com/junobuild/examples
cd typescript/hooks

2. Install dependencies:

npm install

3. Start Juno local emulator:

important

Requires the Juno CLI to be available npm i -g @junobuild/cli

juno dev start

4. Create a Satellite for local dev:

  1. Create required collections:
  1. Start the frontend dev server (in a separate terminal):
npm run dev
  1. Build the serverless functions (in a separate terminal):
juno functions build

The emulator will automatically upgrade your Satellite and live reload the changes.


Juno-Specific Configuration

  • juno.config.ts: Defines Satellite IDs for development/production, build source, and predeploy steps. See the Configuration reference for details.
  • vite.config.ts: Registers the juno plugin to inject environment variables automatically. See the Vite Plugin reference for more information.

Production Deployment

  • Create a Satellite on the Juno Console for mainnet.
  • Update juno.config.ts with the production Satellite ID.
  • Build and deploy the frontend:
npm run build
juno deploy
  • Build and upgrade the serverless functions:
juno functions build
juno functions upgrade

Notes

  • This example focuses on the TypeScript serverless function. The frontend is intentionally minimal and included only for demonstration.
  • Use this project as a starting point for writing custom backend logic in TypeScript using Juno hooks.

Real-World Example

Want to see how assertions and serverless logic are used in a live project?

Check out cycles.watch, an open-source app built with Juno:

This app uses:

  • assertSetDoc to validate requests
  • onSetDoc to implement a swap-like feature that performs various canister calls
  • Service modules to keep logic organized
  • A real-world pattern for chaining calls and document insertions with assertions

It’s a great reference for more advanced setups and orchestration.


References