diff --git a/apps/fullstack/components/context/viewer/useWorkerContext.tsx b/apps/fullstack/components/context/viewer/useWorkerContext.tsx
index 1f729a5b4829482233a61e64692287ce938d04db..099ce02e9a57b21b0d30d3561c3d8fea7847f204 100644
--- a/apps/fullstack/components/context/viewer/useWorkerContext.tsx
+++ b/apps/fullstack/components/context/viewer/useWorkerContext.tsx
@@ -34,6 +34,7 @@ export function WorkerContextProvider({
 
     /* Scaling and create the workers dynamically */
     useEffect(() => {
+        if (!socket) return;
         setWorker((w) => {
             //Scale up
             if (w.size < n) {
diff --git a/apps/fullstack/components/editor/index.tsx b/apps/fullstack/components/editor/index.tsx
index bc5d1bff36c428eeb9ba56e6e5a7615b64c1a7c8..d17a970832572639e11464f56cc912b8631941e1 100644
--- a/apps/fullstack/components/editor/index.tsx
+++ b/apps/fullstack/components/editor/index.tsx
@@ -74,5 +74,6 @@ function getAllowedTools(perms: Perms) {
         tools.push("placement");
         tools.push("image");
     }
+    tools.push("laserPointer");
     return tools;
 }
diff --git a/apps/fullstack/components/editor/tools/doodle/context.tsx b/apps/fullstack/components/editor/tools/doodle/context.tsx
index bfad6983fc645065f46f6e4be3ce88bf83f04941..22461660ed5957d0892a728a6d534a7a6b1794c8 100644
--- a/apps/fullstack/components/editor/tools/doodle/context.tsx
+++ b/apps/fullstack/components/editor/tools/doodle/context.tsx
@@ -56,7 +56,6 @@ export function DoodleToolContextProvider({ children }: ToolContextProps) {
     const { config } = useUser(true);
     const mounted = useRef(false);
 
-    // TODO: sync with database
     const [colorList, setColorList] = useState([
         "#000000",
         "#0000ff",
diff --git a/apps/fullstack/components/editor/tools/laserPointer/context.tsx b/apps/fullstack/components/editor/tools/laserPointer/context.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..fbde324ea5d416da68143e1e30323c9a392648e4
--- /dev/null
+++ b/apps/fullstack/components/editor/tools/laserPointer/context.tsx
@@ -0,0 +1,29 @@
+import { createContext, useContext } from "react";
+
+import { type ID } from "@snip/database/types";
+
+import { ToolContextProps } from "../types";
+
+export interface PointerContextI {
+    sendPos: (xy_db: [number, number], pageId: ID) => void;
+}
+
+const PointerContext = createContext<PointerContextI>({
+    sendPos: () => {},
+});
+
+export function PointerContextProvider({ children }: ToolContextProps) {
+    function sendPos(xy_db, pageId) {
+        console.log(pageId);
+    }
+
+    return (
+        <PointerContext.Provider value={{ sendPos }}>
+            {children}
+        </PointerContext.Provider>
+    );
+}
+
+export function usePointerContext() {
+    return useContext(PointerContext);
+}
diff --git a/apps/fullstack/components/editor/tools/laserPointer/index.tsx b/apps/fullstack/components/editor/tools/laserPointer/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..60d7a83bfb281975f8f4174633f6871c5c5585db
--- /dev/null
+++ b/apps/fullstack/components/editor/tools/laserPointer/index.tsx
@@ -0,0 +1,76 @@
+import { useCallback, useEffect, useRef } from "react";
+import { createPortal } from "react-dom";
+import { TbPoint } from "react-icons/tb";
+
+import { Toggle } from "../common/toggle";
+import { ToolComponents, ToolToggleProps } from "../types";
+import { PointerContextProvider, usePointerContext } from "./context";
+
+import { useWorkerContext } from "components/context/viewer/useWorkerContext";
+import { useEditorContext } from "components/editor/context";
+
+function PointerToggle(props: ToolToggleProps) {
+    return <Toggle label="Pointer" icon={<TbPoint />} {...props} />;
+}
+
+function PointerHelp() {
+    return null;
+}
+
+function PointerOverlay() {
+    const { pageWrappersRef } = useEditorContext();
+    const cursorRef = useRef<HTMLDivElement>();
+    const { overlayWorker } = useWorkerContext();
+    /** The move event gets propagated threw the socket connection
+     * to other clients which joined the current page. Positions
+     * are send in db coords.
+     */
+    const handleMove = useCallback(
+        (e: PointerEvent, idx: number) => {
+            // Transmit position to other clients in real-time
+            const xy: [number, number] = [e.offsetX, e.offsetY];
+            const w = overlayWorker.get(idx);
+            if (!w) return;
+            const xy_db = w.canvasCoords_2_dbCoords(xy);
+            w.laserPointerHandler.sendPos(xy_db);
+        },
+        [overlayWorker],
+    );
+
+    useEffect(() => {
+        const pages = Array.from(
+            pageWrappersRef.current?.values(),
+            (wrapper) => wrapper.children[0] as HTMLDivElement,
+        );
+
+        const moveHandlers = pages.map((page, idx) => {
+            return (e) => handleMove(e, idx);
+        });
+
+        pages.forEach((page, idx) => {
+            page.addEventListener("pointermove", moveHandlers[idx]);
+
+            page.style.cursor = "crosshair";
+        });
+
+        return () => {
+            pages.forEach((page, idx) => {
+                page.removeEventListener("pointermove", moveHandlers[idx]);
+
+                page.style.cursor = "default";
+            });
+        };
+    }, [handleMove, pageWrappersRef]);
+
+    return null;
+}
+
+const PointerTool: ToolComponents = {
+    name: "laserPointer" as const,
+    Toggle: PointerToggle,
+    Help: PointerHelp,
+    Overlay: PointerOverlay,
+    ContextProvider: PointerContextProvider,
+};
+
+export default PointerTool;
diff --git a/apps/fullstack/components/editor/tools/toolsFactory.tsx b/apps/fullstack/components/editor/tools/toolsFactory.tsx
index 82f0a42ceae42f3c813e2d8bd6cd7bb45472e887..b33c59ddc4cba2bf17a27fccfe1d3adebd936b8a 100644
--- a/apps/fullstack/components/editor/tools/toolsFactory.tsx
+++ b/apps/fullstack/components/editor/tools/toolsFactory.tsx
@@ -2,6 +2,7 @@
 import DoodleTool from "./doodle";
 import ImageTool from "./imageEditor";
 import InteractionTool from "./interaction";
+import PointerTool from "./laserPointer";
 import MoveTool from "./move";
 import PlacementTool from "./placement";
 import TextTool from "./text";
@@ -21,6 +22,8 @@ export function getToolComponents(tool: Tool): ToolComponents {
             return ImageTool;
         case "placement":
             return PlacementTool;
+        case "laserPointer":
+            return PointerTool;
         default:
             throw new Error("Unknown tool");
     }
diff --git a/apps/fullstack/components/editor/tools/types.ts b/apps/fullstack/components/editor/tools/types.ts
index 8cfadbc803dc8b61ce7a078eead6abeeba70dd5b..39a8e14b1741cab87922e05113f38369b69d7dfe 100644
--- a/apps/fullstack/components/editor/tools/types.ts
+++ b/apps/fullstack/components/editor/tools/types.ts
@@ -5,7 +5,8 @@ export type Tool =
     | "snipEditor"
     | "text"
     | "image"
-    | "placement";
+    | "placement"
+    | "laserPointer";
 
 export interface ToolToggleProps {
     isActive: boolean;
diff --git a/apps/fullstack/components/editor/worker/overlayWorker.tsx b/apps/fullstack/components/editor/worker/overlayWorker.tsx
index 59cfe9006f154699c692a8c671806e45b48e8b6c..a5915807a2db73137348259f6fec17b1f3d5a3df 100644
--- a/apps/fullstack/components/editor/worker/overlayWorker.tsx
+++ b/apps/fullstack/components/editor/worker/overlayWorker.tsx
@@ -11,7 +11,7 @@
  */
 import { Socket } from "socket.io-client";
 
-import type { PageData } from "@snip/database/types";
+import type { ID, PageData } from "@snip/database/types";
 import { BaseData, BaseView, SnipData } from "@snip/snips/general/base";
 import { get_snip_from_data } from "@snip/snips/get_snip_from_data";
 
@@ -21,17 +21,28 @@ const debug = (...args: unknown[]) => {
     console.debug("[OverlayWorker]", ...args);
 };
 
+type RenderFunction = (ctx: CanvasRenderingContext2D) => void;
+
 export class OverlayWorker extends RenderWorker {
     private ctx?: CanvasRenderingContext2D;
-    private stack = new Set<(ctx: CanvasRenderingContext2D) => void>();
+    private stack = new Set<RenderFunction>();
     private socket?: Socket;
 
     // Canvas size in db coordinates
     private max: [number, number] = [1400, 2000];
 
+    public laserPointerHandler: LaserPointerHandler;
     constructor(socket: Socket) {
         super();
         this.socket = socket;
+
+        if (!socket) {
+            return;
+        }
+
+        // Attach socket handler(s)
+        this.socket.on("snip:preview", this.onSnipPreview);
+        this.laserPointerHandler = new LaserPointerHandler(this, socket);
     }
 
     public async setupCanvas(canvas: HTMLCanvasElement) {
@@ -85,7 +96,7 @@ export class OverlayWorker extends RenderWorker {
         number,
         (ctx: CanvasRenderingContext2D) => void
     >();
-    public receivePreviewData(snipData: SnipData<BaseData, BaseView>) {
+    public onSnipPreview(snipData: SnipData<BaseData, BaseView>) {
         if (!this.current_page_id || snipData.page_id !== this.current_page_id)
             return;
 
@@ -192,3 +203,109 @@ export class OverlayWorker extends RenderWorker {
         return this.canvas.height / 2000;
     }
 }
+
+/** Render handler for the laserpointer tool
+ * i.e. communicate new positions and
+ * render pointer events.
+ */
+class LaserPointerHandler {
+    worker: OverlayWorker;
+    socket: Socket;
+
+    // We keep a list of received laser pointer events
+    laserPointerEvents: {
+        xy: [number, number];
+        timestamp: number;
+        socket_id: string;
+        user_email: string;
+    }[];
+
+    map: Map<string, { xy: [number, number]; timestamp: number }[]>;
+
+    constructor(worker: OverlayWorker, socket: Socket) {
+        this.worker = worker;
+        this.socket = socket;
+        this.laserPointerEvents = [];
+        this.map = new Map();
+        socket.on("laserPointer:pos", this.onPos.bind(this));
+    }
+
+    private onPos(
+        xy: [number, number],
+        pageId: ID,
+        socket_id: string,
+        user_email: string,
+        timestamp: number,
+    ) {
+        if (pageId != this.worker.current_page_id) return;
+
+        // append to socket data
+        const socket_data = this.map.get(socket_id) || [];
+        socket_data.push({ xy, timestamp });
+        this.map.set(socket_id, socket_data);
+
+        // Launch render
+        if (!this.renderFkt) {
+            this.renderFkt = this.render.bind(this);
+            this.worker.add(this.renderFkt);
+            this.worker.rerender();
+        }
+    }
+
+    public sendPos(xy: [number, number]) {
+        if (!this.socket) return;
+        this.socket.emit("laserPointer:pos", xy, this.worker.current_page_id);
+    }
+
+    renderFkt: RenderFunction;
+
+    // Keep lines for 1s
+    lineDuration = 1000;
+    // Keep circles for 10s
+    circleDuration = 5000;
+
+    /** Render all laserPointer events */
+    private render(ctx: CanvasRenderingContext2D) {
+        const now = Date.now();
+
+        for (const [socket_id, socket_data] of this.map.entries()) {
+            // For each socket we draw a a line
+            ctx.beginPath();
+            ctx.strokeStyle = "rgba(255, 0, 0, 0.2)";
+            ctx.lineCap = "round";
+            ctx.lineWidth = 10;
+            for (let i = socket_data.length - 2; i >= 0; i--) {
+                ctx.lineTo(socket_data[i].xy[0], socket_data[i].xy[1]);
+
+                // Splice after duration is up
+                if (socket_data[i].timestamp < now - this.lineDuration)
+                    socket_data.splice(i, 1);
+            }
+            ctx.stroke();
+
+            //We also draw a bigger circle on the current position
+            const last_data = socket_data.at(-1);
+            if (!last_data) {
+                this.map.delete(socket_id);
+                continue;
+            }
+
+            ctx.beginPath();
+            ctx.fillStyle = "rgba(255,0,0,0.5)";
+            ctx.arc(last_data.xy[0], last_data.xy[1], 10, 0, Math.PI * 2);
+            ctx.fill();
+
+            // Splice after duration is up
+            if (last_data.timestamp < now - this.circleDuration)
+                socket_data.pop();
+        }
+        requestAnimationFrame(() => {
+            if (this.map.size > 0) {
+                this.worker.rerender();
+            } else {
+                this.worker.remove(this.renderFkt);
+                this.renderFkt = null;
+            }
+        });
+    }
+}
diff --git a/apps/fullstack/package.json b/apps/fullstack/package.json
index 8b372f0f56fdc313235917c5f9a8ead9191bf78a..01cf79eb34f47e922c15c4c642ef0c4bff238bdb 100644
--- a/apps/fullstack/package.json
+++ b/apps/fullstack/package.json
@@ -52,8 +52,8 @@
         "ts-node": "^10.9.2",
         "tsconfig-paths": "^4.2.0",
         "tsconfig-paths-jest": "^0.0.1",
-        "tslib": "^2.6.3",
-        "typescript": "5.3.3"
+        "typescript": "^5.5.2",
+        "tslib": "^2.6.3"
     },
     "scripts": {
         "lint": "pnpm eslint .",
diff --git a/apps/socket/src/prefixes/laserPointer/socket.ts b/apps/socket/src/prefixes/laserPointer/socket.ts
new file mode 100644
index 0000000000000000000000000000000000000000..415e122923afb7e720546d4d0d58acfc67af0abe
--- /dev/null
+++ b/apps/socket/src/prefixes/laserPointer/socket.ts
@@ -0,0 +1,28 @@
+import { type SocketData } from "../../types";
+import { allowedPage } from "../../utils/auth";
+
+export function setupLaserPointerPrefix({ socket }: SocketData) {
+    /** Receive a new laser pointer positions and propagate it to all
+     * other sockets
+     */
+    socket.on("laserPointer:pos", async (xy: [number, number], pageId) => {
+        if (!allowedPage(socket, pageId)) {
+            return socket.emit("error", {
+                message: "Unauthorized access to page",
+            });
+        }
+
+        //logWS(socket, "laserPointer", socket.data.id);
+
+        socket
+            .to("page-" + String(pageId))
+            .emit(
+                "laserPointer:pos",
+                xy,
+                pageId,
+                socket.id,
+                socket.data.email,
+                Date.now(),
+            );
+    });
+}
diff --git a/apps/socket/src/prefixes/setupNamespace.ts b/apps/socket/src/prefixes/setupNamespace.ts
index cb9185c693785be77470ca443391c29918a94e1d..5e030ee1dfa64835875f56d7915018a533646298 100644
--- a/apps/socket/src/prefixes/setupNamespace.ts
+++ b/apps/socket/src/prefixes/setupNamespace.ts
@@ -12,6 +12,7 @@ import {
 } from "../utils/auth";
 import { logAPI, logWS } from "../utils/logging";
 import { setupBookPrefix } from "./book/socket";
+import { setupLaserPointerPrefix } from "./laserPointer/socket";
 import { setupPagesPrefix } from "./page/socket";
 import { setupSnipPrefix } from "./snip/socket";
 import { setupUsersPrefix } from "./users/socket";
@@ -61,6 +62,11 @@ export function setupNamespaces(server: Server) {
          * /book-[book_id]/users:[function]/
          */
         setupUsersPrefix(data);
+
+        /**Setup all functions for laser pointer prefix
+         *
+         */
+        setupLaserPointerPrefix(data);
     });
 
     setupServerSpace(server);
diff --git a/apps/socket/src/prefixes/snip/socket.ts b/apps/socket/src/prefixes/snip/socket.ts
index f55c41baf057389544f4d8b258d6f5023e3bd736..c0925c227e7b7030dd0fb4a2474c5622b1ef93b8 100644
--- a/apps/socket/src/prefixes/snip/socket.ts
+++ b/apps/socket/src/prefixes/snip/socket.ts
@@ -3,6 +3,7 @@ import { pool_data } from "@snip/database/sql.connection";
 import { BaseData, BaseView, SnipData } from "@snip/snips/general/base";
 
 import { type SocketData } from "../../types";
+import { allowedPage } from "../../utils/auth";
 import { logWS } from "../../utils/logging";
 import { triggerRender } from "../../utils/triggerRender";
 
@@ -48,6 +49,18 @@ export function setupSnipPrefix({
                 );
             }
 
+            const page_id = snip_data.page_id;
+            // Sanity check for page_id
+            if (page_id === null || page_id === undefined) {
+                return socket.emit("error", "Page ID is missing or invalid.");
+            }
+            if (!allowedPage(socket, page_id)) {
+                return socket.emit(
+                    "error",
+                    "You do not have access to this page.",
+                );
+            }
+
             return await update(
                 { socket, book_nsp, server, ...props },
                 snip_data,
@@ -78,7 +91,7 @@ export function setupSnipPrefix({
 
     socket.on(
         "snip:preview",
-        async (SnipData: SnipData<BaseData, BaseView>) => {
+        async (snipData: SnipData<BaseData, BaseView>) => {
             // Check if user has permission to write to the page
             if (!(socket.perms.pWrite === true)) {
                 return socket.emit(
@@ -88,8 +101,8 @@ export function setupSnipPrefix({
             }
 
             socket
-                .to("page-" + String(SnipData.page_id))
-                .volatile.emit("snip:preview", SnipData);
+                .to("page-" + String(snipData.page_id))
+                .volatile.emit("snip:preview", snipData);
         },
     );
 
@@ -103,6 +116,9 @@ export function setupSnipPrefix({
                 "You do not have access to write to this book.",
             );
         }
+        if (!allowedPage(socket, page_id)) {
+            return socket.emit("error", "You do not have access to this page.");
+        }
 
         logWS(socket, "[Page: " + page_id + "] snip:placed " + snip_id);
 
@@ -114,19 +130,13 @@ export function setupSnipPrefix({
 }
 
 async function insert(
-    { socket, book_nsp, server }: SocketData,
+    { socket, server }: SocketData,
     snip_data: SnipData<BaseData, BaseView>,
     callback: (id: number | null) => void,
 ) {
     const page_id = snip_data.page_id;
     logWS(socket, "[Page: " + page_id + "] snip:insert");
 
-    // Sanity check for page_id
-    if (page_id === null || page_id === undefined) {
-        console.error("[Socket][Book: " + socket.book_id + "] Page id is null");
-        return;
-    }
-
     // Sanity check book id
     snip_data.book_id = socket.book_id;
     snip_data.created_by = socket.perms.entity_id;
@@ -162,11 +172,6 @@ async function update(
 ) {
     const page_id = snip_data.page_id;
 
-    // Sanity check for page_id
-    if (page_id === null || page_id === undefined) {
-        console.error("[Socket][Book: " + socket.book_id + "] Page id is null");
-        return;
-    }
     logWS(socket, "[Page: " + page_id + "] snip:update");
 
     // Sanity check book id
diff --git a/apps/socket/src/utils/auth.ts b/apps/socket/src/utils/auth.ts
index 05fcdce71a7a8db5a969f3aa3ab2fb64f0924e7b..354790f7cba151ee5fc45130b4ff07464a0c6c4f 100644
--- a/apps/socket/src/utils/auth.ts
+++ b/apps/socket/src/utils/auth.ts
@@ -163,7 +163,7 @@ export function allowedPage(
     pWrite = false,
     pDelete = false,
 ) {
-    if (!allowed(socket, pRead, pWrite, pDelete)) {
+    if (!allowed(socket, pRead, pWrite, pDelete) || !page_id) {
         return false;
     }
     return socket.data.allowedPages.includes(page_id);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 757ff6a925259b1400e4a985232992457e2c0ba8..b07413ac585b532bea03cb9dac8ed42d6841a524 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -204,6 +204,9 @@ importers:
       swr:
         specifier: ^2.2.5
         version: 2.2.5(react@18.3.1)
+      typescript:
+        specifier: ^5.5.2
+        version: 5.5.2
       zod:
         specifier: ^3.23.8
         version: 3.23.8
@@ -246,7 +249,7 @@ importers:
         version: 1.15.0
       ts-node:
         specifier: ^10.9.2
-        version: 10.9.2(@types/node@20.14.8)(typescript@5.3.3)
+        version: 10.9.2(@types/node@20.14.8)(typescript@5.5.2)
       tsconfig-paths:
         specifier: ^4.2.0
         version: 4.2.0
@@ -256,9 +259,6 @@ importers:
       tslib:
         specifier: ^2.6.3
         version: 2.6.3
-      typescript:
-        specifier: 5.3.3
-        version: 5.3.3
 
   apps/render:
     dependencies:
@@ -9420,37 +9420,6 @@ packages:
       yargs-parser: 21.1.1
     dev: true
 
-  /ts-node@10.9.2(@types/node@20.14.8)(typescript@5.3.3):
-    resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
-    hasBin: true
-    peerDependencies:
-      '@swc/core': '>=1.2.50'
-      '@swc/wasm': '>=1.2.50'
-      '@types/node': '*'
-      typescript: '>=2.7'
-    peerDependenciesMeta:
-      '@swc/core':
-        optional: true
-      '@swc/wasm':
-        optional: true
-    dependencies:
-      '@cspotcode/source-map-support': 0.8.1
-      '@tsconfig/node10': 1.0.11
-      '@tsconfig/node12': 1.0.11
-      '@tsconfig/node14': 1.0.3
-      '@tsconfig/node16': 1.0.4
-      '@types/node': 20.14.8
-      acorn: 8.12.0
-      acorn-walk: 8.3.3
-      arg: 4.1.3
-      create-require: 1.1.1
-      diff: 4.0.2
-      make-error: 1.3.6
-      typescript: 5.3.3
-      v8-compile-cache-lib: 3.0.1
-      yn: 3.1.1
-    dev: true
-
   /ts-node@10.9.2(@types/node@20.14.8)(typescript@5.5.2):
     resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
     hasBin: true
@@ -9665,12 +9634,6 @@ packages:
       possible-typed-array-names: 1.0.0
     dev: true
 
-  /typescript@5.3.3:
-    resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
-    engines: {node: '>=14.17'}
-    hasBin: true
-    dev: true
-
   /typescript@5.5.2:
     resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==}
     engines: {node: '>=14.17'}