diff --git a/include/libcamera/buffer.h b/include/libcamera/buffer.h
index 0b95e41a2dd205b2..f5522edaf68bbf61 100644
--- a/include/libcamera/buffer.h
+++ b/include/libcamera/buffer.h
@@ -11,6 +11,8 @@
 #include <stdint.h>
 #include <vector>
 
+#include <libcamera/file_descriptor.h>
+
 namespace libcamera {
 
 class Request;
@@ -33,6 +35,40 @@ struct FrameMetadata {
 	std::vector<Plane> planes;
 };
 
+class FrameBuffer final
+{
+public:
+	struct Plane {
+		FileDescriptor fd;
+		unsigned int length;
+	};
+
+	FrameBuffer(const std::vector<Plane> &planes, unsigned int cookie = 0);
+	FrameBuffer(FrameBuffer &&other);
+
+	FrameBuffer(const FrameBuffer &other) = delete;
+	FrameBuffer &operator=(const FrameBuffer &) = delete;
+
+	const std::vector<Plane> &planes() const { return planes_; }
+
+	Request *request() const { return request_; }
+	const FrameMetadata &metadata() const { return metadata_; };
+
+	unsigned int cookie() const { return cookie_; }
+	void setCookie(unsigned int cookie) { cookie_ = cookie; }
+
+private:
+	friend class Request; /* Needed to update request_. */
+	friend class V4L2VideoDevice; /* Needed to update metadata_. */
+
+	std::vector<Plane> planes_;
+
+	Request *request_;
+	FrameMetadata metadata_;
+
+	unsigned int cookie_;
+};
+
 class Plane final
 {
 public:
diff --git a/src/libcamera/buffer.cpp b/src/libcamera/buffer.cpp
index b7bc948c80748b18..3e2cc9b94595795e 100644
--- a/src/libcamera/buffer.cpp
+++ b/src/libcamera/buffer.cpp
@@ -240,7 +240,7 @@ void *Plane::mem()
 }
 
 /**
- * \fn Plane::length()
+ * \fn Plane::length() const
  * \brief Retrieve the length of the memory region
  * \return The length of the memory region
  */
@@ -473,4 +473,132 @@ void Buffer::cancel()
  * The intended callers are Request::prepare() and Request::completeBuffer().
  */
 
+/**
+ * \class FrameBuffer
+ * \brief Frame buffer data and its associated dynamic metadata
+ *
+ * The FrameBuffer class is the primary interface for applications, IPAs and
+ * pipelines to interact with frame memory. It contains all the static and
+ * dynamic information to manage the whole life cycle of a frame capture, from
+ * buffer creation to consumption.
+ *
+ * The static information describes the memory planes that make a frame. The
+ * planes are specified when creating the FrameBuffer and are expressed as a set
+ * of dmabuf file descriptors and length.
+ *
+ * The dynamic information is grouped in a FrameMetadata instance. It is updated
+ * during the processing of a queued capture request, and is valid from the
+ * completion of the buffer as signaled by Camera::bufferComplete() until the
+ * FrameBuffer is either reused in a new request or deleted.
+ *
+ * The creator of a FrameBuffer (application, IPA or pipeline) may associate
+ * to it an integer cookie for any private purpose. The cookie may be set when
+ * creating the FrameBuffer, and updated at any time with setCookie(). The
+ * cookie is transparent to the libcamera core and shall only be set by the
+ * creator of the FrameBuffer. This mechanism supplements the Request cookie.
+ */
+
+/**
+ * \struct FrameBuffer::Plane
+ * \brief A memory region to store a single plane of a frame
+ *
+ * Planar pixel formats use multiple memory regions to store the different
+ * colour components of a frame. The Plane structure describes such a memory
+ * region by a dmabuf file descriptor and a length. A FrameBuffer then
+ * contains one or multiple planes, depending on the pixel format of the
+ * frames it is meant to store.
+ *
+ * To support DMA access, planes are associated with dmabuf objects represented
+ * by FileDescriptor handles. The Plane class doesn't handle mapping of the
+ * memory to the CPU, but applications and IPAs may use the dmabuf file
+ * descriptors to map the plane memory with mmap() and access its contents.
+ *
+ * \todo Once we have a Kernel API which can express offsets within a plane
+ * this structure shall be extended to contain this information. See commit
+ * 83148ce8be55e for initial documentation of this feature.
+ */
+
+/**
+ * \var FrameBuffer::Plane::fd
+ * \brief The dmabuf file descriptor
+ */
+
+/**
+ * \var FrameBuffer::Plane::length
+ * \brief The plane length in bytes
+ */
+
+/**
+ * \brief Construct a FrameBuffer with an array of planes
+ * \param[in] planes The frame memory planes
+ * \param[in] cookie Cookie
+ *
+ * The file descriptors associated with each plane are duplicated and can be
+ * closed by the caller after the FrameBuffer has been constructed.
+ */
+FrameBuffer::FrameBuffer(const std::vector<Plane> &planes, unsigned int cookie)
+	: planes_(planes), request_(nullptr), cookie_(cookie)
+{
+}
+
+/**
+ * \brief Move constructor, create a FrameBuffer by taking over \a other
+ * \param[in] other The other FrameBuffer
+ *
+ * The \a other FrameBuffer will be invalidated.
+ */
+FrameBuffer::FrameBuffer(FrameBuffer &&other)
+	: planes_(std::move(other.planes_)),
+	  request_(utils::exchange(other.request_, nullptr)),
+	  metadata_(std::move(other.metadata_)),
+	  cookie_(utils::exchange(other.cookie_, 0))
+{
+}
+
+/**
+ * \fn FrameBuffer::planes()
+ * \brief Retrieve the static plane descriptors
+ * \return Array of plane descriptors
+ */
+
+/**
+ * \fn FrameBuffer::request()
+ * \brief Retrieve the request this buffer belongs to
+ *
+ * The intended callers of this method are buffer completion handlers that
+ * need to associate a buffer to the request it belongs to.
+ *
+ * \return The Request the Buffer belongs to, or nullptr if the buffer is
+ * not associated with a request
+ */
+
+/**
+ * \fn FrameBuffer::metadata()
+ * \brief Retrieve the dynamic metadata
+ * \return Dynamic metadata for the frame contained in the buffer
+ */
+
+/**
+ * \fn FrameBuffer::cookie()
+ * \brief Retrieve the cookie
+ *
+ * The cookie belongs to the creator of the FrameBuffer, which controls its
+ * lifetime and value.
+ *
+ * \sa setCookie()
+ *
+ * \return The cookie
+ */
+
+/**
+ * \fn FrameBuffer::setCookie()
+ * \brief Set the cookie
+ * \param[in] cookie Cookie to set
+ *
+ * The cookie belongs to the creator of the FrameBuffer. Its value may be
+ * modified at any time with this method. Applications and IPAs shall not modify
+ * the cookie value of buffers they haven't created themselves. The libcamera
+ * core never modifies the buffer cookie.
+ */
+
 } /* namespace libcamera */
