diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py
index ef38904c0..f19f81022 100644
--- a/backend/open_webui/main.py
+++ b/backend/open_webui/main.py
@@ -64,6 +64,7 @@ from open_webui.routers import (
auths,
channels,
chats,
+ notes,
folders,
configs,
groups,
@@ -996,6 +997,8 @@ app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
app.include_router(channels.router, prefix="/api/v1/channels", tags=["channels"])
app.include_router(chats.router, prefix="/api/v1/chats", tags=["chats"])
+app.include_router(notes.router, prefix="/api/v1/notes", tags=["notes"])
+
app.include_router(models.router, prefix="/api/v1/models", tags=["models"])
app.include_router(knowledge.router, prefix="/api/v1/knowledge", tags=["knowledge"])
diff --git a/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py b/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py
new file mode 100644
index 000000000..8e983a2cf
--- /dev/null
+++ b/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py
@@ -0,0 +1,33 @@
+"""Add note table
+
+Revision ID: 9f0c9cd09105
+Revises: 3781e22d8b01
+Create Date: 2025-05-03 03:00:00.000000
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+revision = "9f0c9cd09105"
+down_revision = "3781e22d8b01"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.create_table(
+ "note",
+ sa.Column("id", sa.Text(), nullable=False, primary_key=True, unique=True),
+ sa.Column("user_id", sa.Text(), nullable=True),
+ sa.Column("title", sa.Text(), nullable=True),
+ sa.Column("data", sa.JSON(), nullable=True),
+ sa.Column("meta", sa.JSON(), nullable=True),
+ sa.Column("access_control", sa.JSON(), nullable=True),
+ sa.Column("created_at", sa.BigInteger(), nullable=True),
+ sa.Column("updated_at", sa.BigInteger(), nullable=True),
+ )
+
+
+def downgrade():
+ op.drop_table("note")
diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py
new file mode 100644
index 000000000..114ccdc57
--- /dev/null
+++ b/backend/open_webui/models/notes.py
@@ -0,0 +1,135 @@
+import json
+import time
+import uuid
+from typing import Optional
+
+from open_webui.internal.db import Base, get_db
+from open_webui.utils.access_control import has_access
+from open_webui.models.users import Users, UserResponse
+
+
+from pydantic import BaseModel, ConfigDict
+from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
+from sqlalchemy import or_, func, select, and_, text
+from sqlalchemy.sql import exists
+
+####################
+# Note DB Schema
+####################
+
+
+class Note(Base):
+ __tablename__ = "note"
+
+ id = Column(Text, primary_key=True)
+ user_id = Column(Text)
+
+ title = Column(Text)
+ data = Column(JSON, nullable=True)
+ meta = Column(JSON, nullable=True)
+
+ access_control = Column(JSON, nullable=True)
+
+ created_at = Column(BigInteger)
+ updated_at = Column(BigInteger)
+
+
+class NoteModel(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: str
+ user_id: str
+
+ title: str
+ data: Optional[dict] = None
+ meta: Optional[dict] = None
+
+ access_control: Optional[dict] = None
+
+ created_at: int # timestamp in epoch
+ updated_at: int # timestamp in epoch
+
+
+####################
+# Forms
+####################
+
+
+class NoteForm(BaseModel):
+ title: str
+ data: Optional[dict] = None
+ meta: Optional[dict] = None
+ access_control: Optional[dict] = None
+
+
+class NoteUserResponse(NoteModel):
+ user: Optional[UserResponse] = None
+
+
+class NoteTable:
+ def insert_new_note(
+ self,
+ form_data: NoteForm,
+ user_id: str,
+ ) -> Optional[NoteModel]:
+ with get_db() as db:
+ note = NoteModel(
+ **{
+ "id": str(uuid.uuid4()),
+ "user_id": user_id,
+ **form_data.model_dump(),
+ "created_at": int(time.time_ns()),
+ "updated_at": int(time.time_ns()),
+ }
+ )
+
+ new_note = Note(**note.model_dump())
+
+ db.add(new_note)
+ db.commit()
+ return note
+
+ def get_notes(self) -> list[NoteModel]:
+ with get_db() as db:
+ notes = db.query(Note).order_by(Note.updated_at.desc()).all()
+ return [NoteModel.model_validate(note) for note in notes]
+
+ def get_notes_by_user_id(
+ self, user_id: str, permission: str = "write"
+ ) -> list[NoteModel]:
+ notes = self.get_notes()
+ return [
+ note
+ for note in notes
+ if note.user_id == user_id
+ or has_access(user_id, permission, note.access_control)
+ ]
+
+ def get_note_by_id(self, id: str) -> Optional[NoteModel]:
+ with get_db() as db:
+ note = db.query(Note).filter(Note.id == id).first()
+ return NoteModel.model_validate(note) if note else None
+
+ def update_note_by_id(self, id: str, form_data: NoteForm) -> Optional[NoteModel]:
+ with get_db() as db:
+ note = db.query(Note).filter(Note.id == id).first()
+ if not note:
+ return None
+
+ note.title = form_data.title
+ note.data = form_data.data
+ note.meta = form_data.meta
+ note.access_control = form_data.access_control
+ note.updated_at = int(time.time_ns())
+
+ db.commit()
+ return NoteModel.model_validate(note) if note else None
+
+ def delete_note_by_id(self, id: str):
+ with get_db() as db:
+ db.query(Note).filter(Note.id == id).delete()
+ db.commit()
+ return True
+
+
+Notes = NoteTable()
diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py
new file mode 100644
index 000000000..ed373e1b5
--- /dev/null
+++ b/backend/open_webui/routers/notes.py
@@ -0,0 +1,163 @@
+import json
+import logging
+from typing import Optional
+
+
+from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks
+from pydantic import BaseModel
+
+from open_webui.models.users import Users, UserNameResponse
+from open_webui.models.notes import Notes, NoteModel, NoteForm, NoteUserResponse
+
+from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
+from open_webui.constants import ERROR_MESSAGES
+from open_webui.env import SRC_LOG_LEVELS
+
+
+from open_webui.utils.auth import get_admin_user, get_verified_user
+from open_webui.utils.access_control import has_access
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["MODELS"])
+
+router = APIRouter()
+
+############################
+# GetNotes
+############################
+
+
+@router.get("/", response_model=list[NoteUserResponse])
+async def get_notes(user=Depends(get_verified_user)):
+ notes = [
+ NoteUserResponse(
+ **{
+ **note.model_dump(),
+ "user": UserNameResponse(
+ **Users.get_user_by_id(note.user_id).model_dump()
+ ),
+ }
+ )
+ for note in Notes.get_notes_by_user_id(user.id, "write")
+ ]
+
+ return notes
+
+
+@router.get("/list", response_model=list[NoteUserResponse])
+async def get_note_list(user=Depends(get_verified_user)):
+ notes = [
+ NoteUserResponse(
+ **{
+ **note.model_dump(),
+ "user": UserNameResponse(
+ **Users.get_user_by_id(note.user_id).model_dump()
+ ),
+ }
+ )
+ for note in Notes.get_notes_by_user_id(user.id, "read")
+ ]
+
+ return notes
+
+
+############################
+# CreateNewNote
+############################
+
+
+@router.post("/create", response_model=Optional[NoteModel])
+async def create_new_note(form_data: NoteForm, user=Depends(get_admin_user)):
+ try:
+ note = Notes.insert_new_note(form_data, user.id)
+ return note
+ except Exception as e:
+ log.exception(e)
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+
+############################
+# GetNoteById
+############################
+
+
+@router.get("/{id}", response_model=Optional[NoteModel])
+async def get_note_by_id(id: str, user=Depends(get_verified_user)):
+ note = Notes.get_note_by_id(id)
+ if not note:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
+ )
+
+ if user.role != "admin" and not has_access(
+ user.id, type="read", access_control=note.access_control
+ ):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+ return note
+
+
+############################
+# UpdateNoteById
+############################
+
+
+@router.post("/{id}/update", response_model=Optional[NoteModel])
+async def update_note_by_id(
+ id: str, form_data: NoteForm, user=Depends(get_verified_user)
+):
+ note = Notes.get_note_by_id(id)
+ if not note:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
+ )
+
+ if user.role != "admin" and not has_access(
+ user.id, type="write", access_control=note.access_control
+ ):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+ try:
+ note = Notes.update_note_by_id(id, form_data)
+ return note
+ except Exception as e:
+ log.exception(e)
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+
+############################
+# DeleteNoteById
+############################
+
+
+@router.delete("/{id}/delete", response_model=bool)
+async def delete_note_by_id(id: str, user=Depends(get_verified_user)):
+ note = Notes.get_note_by_id(id)
+ if not note:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
+ )
+
+ if user.role != "admin" and not has_access(
+ user.id, type="write", access_control=note.access_control
+ ):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
+ )
+
+ try:
+ note = Notes.delete_note_by_id(id)
+ return True
+ except Exception as e:
+ log.exception(e)
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+ )
diff --git a/src/lib/apis/notes/index.ts b/src/lib/apis/notes/index.ts
new file mode 100644
index 000000000..445697fd3
--- /dev/null
+++ b/src/lib/apis/notes/index.ts
@@ -0,0 +1,168 @@
+import { WEBUI_API_BASE_URL } from '$lib/constants';
+
+type NoteItem = {
+ title: string;
+ content: string;
+ access_control?: null | object;
+};
+
+export const createNewNote = async (token: string, note: NoteItem) => {
+ let error = null;
+
+ const res = await fetch(`${WEBUI_API_BASE_URL}/notes/create`, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ authorization: `Bearer ${token}`
+ },
+ body: JSON.stringify({
+ ...note
+ })
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .catch((err) => {
+ error = err.detail;
+ console.log(err);
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+};
+
+export const getNotes = async (token: string = '') => {
+ let error = null;
+
+ const res = await fetch(`${WEBUI_API_BASE_URL}/notes/`, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ authorization: `Bearer ${token}`
+ }
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .then((json) => {
+ return json;
+ })
+ .catch((err) => {
+ error = err.detail;
+ console.log(err);
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+};
+
+export const getNoteById = async (token: string, id: string) => {
+ let error = null;
+
+ const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}`, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ authorization: `Bearer ${token}`
+ }
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .then((json) => {
+ return json;
+ })
+ .catch((err) => {
+ error = err.detail;
+
+ console.log(err);
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+};
+
+export const updateNoteById = async (token: string, id: string, note: NoteItem) => {
+ let error = null;
+
+ const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}/update`, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ authorization: `Bearer ${token}`
+ },
+ body: JSON.stringify({
+ ...note
+ })
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .then((json) => {
+ return json;
+ })
+ .catch((err) => {
+ error = err.detail;
+
+ console.log(err);
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+};
+
+export const deleteNoteById = async (token: string, id: string) => {
+ let error = null;
+
+ const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}/delete`, {
+ method: 'DELETE',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ authorization: `Bearer ${token}`
+ }
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .then((json) => {
+ return json;
+ })
+ .catch((err) => {
+ error = err.detail;
+
+ console.log(err);
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+};
diff --git a/src/lib/components/admin/Functions.svelte b/src/lib/components/admin/Functions.svelte
index 87f4958d4..fbeb61596 100644
--- a/src/lib/components/admin/Functions.svelte
+++ b/src/lib/components/admin/Functions.svelte
@@ -192,7 +192,7 @@