← All @molecule/* packages · App templates
@molecule/api-resource-threadAPI resource · threads · API (Node) · v1.0.1 · Apache-2.0
Threaded discussions with messages, read-tracking, and unread counts for any resource
npm install @molecule/api-resource-thread@molecule/api-resource-thread is an API resource: the routes, validation and storage for threads, built on the database and auth cores so it runs on whichever providers your app has bonded.
import { routes, requestHandlerMap } from '@molecule/api-resource-thread'
// Wire routes into your Express app via mlcl inject
// POST /threads
// GET /threads
// GET /threads/unread
// GET /threads/:threadId
// PATCH /threads/:threadId
// DELETE /threads/:threadId
// GET /threads/:threadId/messages
// POST /threads/:threadId/messages
// PUT /threads/messages/:messageId
// DELETE /threads/messages/:messageId
// POST /threads/:threadId/readAuto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Threaded discussion resource for molecule.dev.
Conversation threads with messages, read-tracking, and unread counts.
Threads can optionally attach to any resource via resourceType/resourceId.
import { routes, requestHandlerMap } from '@molecule/api-resource-thread'
// Wire routes into your Express app via mlcl inject
// POST /threads
// GET /threads
// GET /threads/unread
// GET /threads/:threadId
// PATCH /threads/:threadId
// DELETE /threads/:threadId
// GET /threads/:threadId/messages
// POST /threads/:threadId/messages
// PUT /threads/messages/:messageId
// DELETE /threads/messages/:messageId
// POST /threads/:threadId/read
resource
npm install @molecule/api-resource-thread @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource zod
CreateMessageInputInput for creating a new message in a thread.
interface CreateMessageInput {
/** The message body text. */
body: string
}
CreateThreadInputInput for creating a new thread.
interface CreateThreadInput {
/** Display title for the thread. */
title: string
/** Optional resource type to attach the thread to. */
resourceType?: string
/** Optional resource ID to attach the thread to. */
resourceId?: string
}
PaginatedResultA paginated result set.
interface PaginatedResult<T> {
/** The result items for the current page. */
data: T[]
/** Total number of matching items across all pages. */
total: number
/** Maximum number of results per page. */
limit: number
/** Number of results skipped. */
offset: number
}
PaginationOptionsOptions for paginated queries.
interface PaginationOptions {
/** Maximum number of results to return. */
limit?: number
/** Number of results to skip. */
offset?: number
}
ThreadA discussion thread containing messages between participants.
interface Thread {
/** Unique thread identifier. */
id: string
/** Display title for the thread. */
title: string
/** The ID of the user who created the thread. */
creatorId: string
/** Optional resource type this thread is attached to (e.g. 'project', 'order'). */
resourceType: string | null
/** Optional resource ID this thread is attached to. */
resourceId: string | null
/** Whether the thread is closed for new messages. */
closed: boolean
/** When the thread was created (ISO 8601). */
createdAt: string
/** When the thread was last updated (ISO 8601). */
updatedAt: string
}
ThreadMessageA message within a thread.
interface ThreadMessage {
/** Unique message identifier. */
id: string
/** The ID of the thread this message belongs to. */
threadId: string
/** The ID of the user who sent the message. */
userId: string
/** The message body text. */
body: string
/** When the message was last edited, or `null` if never edited. */
editedAt: string | null
/** When the message was created (ISO 8601). */
createdAt: string
/** When the message was last updated (ISO 8601). */
updatedAt: string
}
ThreadQueryOptions for querying threads.
interface ThreadQuery extends PaginationOptions {
/** Filter to only threads attached to this resource type. */
resourceType?: string
/** Filter to only threads attached to this resource ID. */
resourceId?: string
/** Filter to only open or closed threads. */
closed?: boolean
}
ThreadReadStatusTracks the last-read position for a user in a thread.
interface ThreadReadStatus {
/** The thread ID. */
threadId: string
/** The user ID. */
userId: string
/** The ID of the last message read by this user. */
lastReadMessageId: string
/** When this read status was last updated (ISO 8601). */
updatedAt: string
}
UpdateMessageInputInput for updating an existing message.
interface UpdateMessageInput {
/** The updated message body text. */
body: string
}
UpdateThreadInputInput for updating an existing thread.
interface UpdateThreadInput {
/** Updated title for the thread. */
title?: string
/** Whether to close or reopen the thread. */
closed?: boolean
}
addMessage(threadId, userId, data)Adds a message to a thread.
function addMessage(
threadId: string,
userId: string,
data: CreateMessageInput,
): Promise<ThreadMessage | null>
threadId — The thread ID to add the message to.userId — The ID of the user sending the message.data — The message creation input.Returns: The created message, or null if the thread is closed or not found.
create(req, res)Creates a new discussion thread.
function create(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with thread creation body.res — The response object.createMessage(req, res)Adds a new message to a thread.
function createMessage(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with threadId param and message body.res — The response object.createThread(creatorId, data)Creates a new thread.
function createThread(creatorId: string, data: CreateThreadInput): Promise<Thread>
creatorId — The ID of the user creating the thread.data — The thread creation input.Returns: The created thread.
del(req, res)Deletes a thread. Only the thread creator can delete.
function del(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with threadId param.res — The response object.deleteMessage(messageId, userId)Deletes a message. Only the message author can delete.
function deleteMessage(messageId: string, userId: string): Promise<boolean>
messageId — The message ID to delete.userId — The requesting user's ID (must match message author).Returns: true if deleted, false if not found or unauthorized.
deleteMsg(req, res)Deletes a message. Only the message author can delete.
function deleteMsg(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with messageId param.res — The response object.deleteThread(threadId, userId)Deletes a thread. Only the thread creator can delete.
function deleteThread(threadId: string, userId: string): Promise<boolean>
threadId — The thread ID to delete.userId — The requesting user's ID (must match thread creator).Returns: true if deleted, false if not found or unauthorized.
getMessages(threadId, options)Retrieves paginated messages for a thread, ordered by creation date ascending.
function getMessages(
threadId: string,
options?: PaginationOptions,
): Promise<PaginatedResult<ThreadMessage>>
threadId — The thread ID to get messages for.options — Pagination options.Returns: A paginated result of messages.
getThreadById(threadId)Retrieves a single thread by ID.
function getThreadById(threadId: string): Promise<Thread | null>
threadId — The thread ID to look up.Returns: The thread or null if not found.
getThreads(userId, options)Retrieves paginated threads for a user (threads where the user has posted messages).
function getThreads(userId: string, options?: ThreadQuery): Promise<PaginatedResult<Thread>>
userId — The user ID to find threads for.options — Query and pagination options.Returns: A paginated result of threads.
getUnreadCount(userId)Returns the number of unread threads for a user. A thread is unread if it has messages newer than the user's last-read position, or if the user has never read it and it contains messages.
function getUnreadCount(userId: string): Promise<number>
userId — The user ID.Returns: The count of threads with unread messages.
list(req, res)Lists paginated threads for the authenticated user.
function list(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with optional query params for filtering and pagination.res — The response object.listMessages(req, res)Lists paginated messages in a thread.
function listMessages(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with threadId param.res — The response object.markRead(threadId, userId, lastReadMessageId)Marks a thread as read up to a specific message for a user.
function markRead(threadId: string, userId: string, lastReadMessageId: string): Promise<void>
threadId — The thread ID.userId — The user marking the thread as read.lastReadMessageId — The ID of the last message read.markThreadRead(req, res)Marks a thread as read up to a specific message for the authenticated user.
function markThreadRead(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with threadId param and lastReadMessageId in body.res — The response object.read(req, res)Retrieves a single thread by ID.
function read(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with threadId param.res — The response object.unread(_req, res)Returns the number of threads with unread messages for the authenticated user.
function unread(_req: MoleculeRequest, res: MoleculeResponse): Promise<void>
_req — The request object (unused).res — The response object.update(req, res)Updates an existing thread. Only the thread creator can update.
function update(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with threadId param and update body.res — The response object.updateMessage(messageId, userId, data)Updates a message. Only the message author can update.
function updateMessage(
messageId: string,
userId: string,
data: UpdateMessageInput,
): Promise<ThreadMessage | null>
messageId — The message ID to update.userId — The requesting user's ID (must match message author).data — The update input.Returns: The updated message or null if not found or unauthorized.
updateMsg(req, res)Updates an existing message. Only the message author can update.
function updateMsg(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
req — The request with messageId param and update body.res — The response object.updateThread(threadId, userId, data)Updates a thread. Only the thread creator can update.
function updateThread(
threadId: string,
userId: string,
data: UpdateThreadInput,
): Promise<Thread | null>
threadId — The thread ID to update.userId — The requesting user's ID (must match thread creator).data — The update input.Returns: The updated thread or null if not found or unauthorized.
createMessageSchemaSchema for validating message creation input.
const createMessageSchema: z.ZodObject<{ body: z.ZodString }, z.core.$strip>
createThreadSchemaSchema for validating thread creation input.
const createThreadSchema: z.ZodObject<
{
title: z.ZodString
resourceType: z.ZodOptional<z.ZodString>
resourceId: z.ZodOptional<z.ZodString>
},
z.core.$strip
>
requestHandlerMapHandler map for thread routes.
const requestHandlerMap: {
readonly create: typeof create
readonly list: typeof list
readonly read: typeof read
readonly update: typeof update
readonly del: typeof del
readonly listMessages: typeof listMessages
readonly createMessage: typeof createMessage
readonly updateMsg: typeof updateMsg
readonly deleteMsg: typeof deleteMsg
readonly markThreadRead: typeof markThreadRead
readonly unread: typeof unread
}
routesRoutes for thread CRUD, messages, and read-tracking.
const routes: readonly [
{
readonly method: 'post'
readonly path: '/threads'
readonly handler: 'create'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/threads'
readonly handler: 'list'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/threads/unread'
readonly handler: 'unread'
readonly middlewares: readonly ['authenticate']
},
{ readonly method: 'get'; readonly path: '/threads/:threadId'; readonly handler: 'read' },
{
readonly method: 'patch'
readonly path: '/threads/:threadId'
readonly handler: 'update'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'delete'
readonly path: '/threads/:threadId'
readonly handler: 'del'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'get'
readonly path: '/threads/:threadId/messages'
readonly handler: 'listMessages'
},
{
readonly method: 'post'
readonly path: '/threads/:threadId/messages'
readonly handler: 'createMessage'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'put'
readonly path: '/threads/messages/:messageId'
readonly handler: 'updateMsg'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'delete'
readonly path: '/threads/messages/:messageId'
readonly handler: 'deleteMsg'
readonly middlewares: readonly ['authenticate']
},
{
readonly method: 'post'
readonly path: '/threads/:threadId/read'
readonly handler: 'markThreadRead'
readonly middlewares: readonly ['authenticate']
},
]
updateMessageSchemaSchema for validating message update input.
const updateMessageSchema: z.ZodObject<{ body: z.ZodString }, z.core.$strip>
updateThreadSchemaSchema for validating thread update input.
const updateThreadSchema: z.ZodObject<
{ title: z.ZodOptional<z.ZodString>; closed: z.ZodOptional<z.ZodBoolean> },
z.core.$strip
>
Peer dependencies:
@molecule/api-database ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-logger ^1.0.1@molecule/api-resource ^1.0.1zod ^4.0.0@molecule/api-database
@molecule/api-i18n
@molecule/api-logger
@molecule/api-resource
zod
List endpoints return a PAGINATED envelope { data, total, limit, offset }, not a
bare array — read the rows off result.data (server). On the client, unwrapList(res)
from @molecule/app-http normalizes this envelope (pass it the whole HttpResponse), so
the rows come back; reading the response as a bare array — or res.data alone (which is
the envelope) — yields an EMPTY list.
SINGLE-USER BY DESIGN — a thread is PRIVATE to its creator. Every read and
write, including GET /threads/:threadId, GET /threads/:threadId/messages,
and POSTING a message, is authorized against thread.creatorId === userId;
any other (or anonymous) caller gets 404 — existence is not leaked. Out of
the box this resource does NOT support multi-participant conversations: for
user-to-user messaging use @molecule/api-resource-message (participant
model), or put your own participant/role gate (e.g. via
@molecule/api-resource-share) in front of these handlers. Messages cannot
be added to a closed thread.
Session-auth prerequisite: handlers read the caller from
res.locals.session.userId and fail closed with 401 — mount the routes
behind your global auth middleware. The two routes declared without an
authenticate middleware (read, listMessages) still require a session
in-handler (defense-in-depth for scanners that drop bare middleware
strings).
Tables: src/__setup__/threads.sql creates threads, thread_messages,
and thread_read_status. An mlcl-scaffolded API replays __setup__/*.sql
automatically on migrate; anywhere else run it once — nothing at runtime
creates them.