diff --git a/src/db.rs b/src/db.rs
index 90144ea..b6b67af 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -82,6 +82,7 @@ impl CommentId {
 	}
 
 	pub fn from_base64(s: &str) -> Result<Self, base64::DecodeError> {
+		// TODO prevent panic when s is too long
 		let mut buf = [0; 16];
 		base64::decode_config_slice(s, base64::URL_SAFE_NO_PAD, &mut buf).map(|_| Self(buf))
 	}
diff --git a/src/helpers.rs b/src/helpers.rs
index 4a537e3..81e9c8f 100644
--- a/src/helpers.rs
+++ b/src/helpers.rs
@@ -1,4 +1,4 @@
-use crate::{config::Config, db::*};
+use crate::{config::Config, db::*, queries::*};
 
 use log::error;
 use std::{net::IpAddr, str::FromStr};
@@ -30,13 +30,18 @@ pub fn approve_comment(comment_id: CommentId, dbs: &Dbs) -> Result<(), sled::Err
 	Ok(())
 }
 
-pub fn remove_pending_comment(
-	comment_id: CommentId,
-	dbs: &Dbs,
-) -> Result<Option<Comment>, sled::Error> {
+pub fn remove_comment(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment>, sled::Error> {
 	if let Some(comment) = dbs.comment.remove(&comment_id)? {
-		dbs.comment_pending
-			.remove(&(comment.topic_hash.clone(), comment.post_time, comment_id))?;
+		dbs.comment_pending.remove(&(
+			comment.topic_hash.clone(),
+			comment.post_time,
+			comment_id.clone(),
+		))?;
+		dbs.comment_approved.remove(&(
+			comment.topic_hash.clone(),
+			comment.post_time,
+			comment_id,
+		))?;
 		return Ok(Some(comment));
 	}
 	Ok(None)
@@ -163,6 +168,30 @@ pub fn get_client_addr<State>(
 	))
 }
 
+pub fn check_comment(config: &Config, comment: &CommentForm, errors: &mut Vec<String>) {
+	if comment.author.len() > config.comment_author_max_len {
+		errors.push(format!(
+			"Author name length is {} but maximum is {}.",
+			comment.author.len(),
+			config.comment_author_max_len
+		));
+	}
+	if comment.email.len() > config.comment_email_max_len {
+		errors.push(format!(
+			"E-mail length is {} but maximum is {}.",
+			comment.email.len(),
+			config.comment_email_max_len
+		));
+	}
+	if comment.text.len() > config.comment_text_max_len {
+		errors.push(format!(
+			"Comment length is {} but maximum is {}.",
+			comment.text.len(),
+			config.comment_text_max_len
+		));
+	}
+}
+
 #[cfg(test)]
 mod test {
 	use super::*;
diff --git a/src/queries.rs b/src/queries.rs
index 21d0d2a..f59a70e 100644
--- a/src/queries.rs
+++ b/src/queries.rs
@@ -1,4 +1,4 @@
-use serde::{Deserialize, Serialize};
+use serde::Deserialize;
 
 #[derive(Clone, Debug, Deserialize)]
 pub struct AdminLoginQuery {
@@ -8,37 +8,35 @@ pub struct AdminLoginQuery {
 #[derive(Clone, Debug, Deserialize)]
 pub struct AdminEditCommentQuery {
 	pub author: String,
-	pub comment_id: String,
+	pub id: String,
 	pub email: String,
 	pub text: String,
 }
 
 #[derive(Clone, Debug, Deserialize)]
 pub struct AdminRmCommentQuery {
-	pub comment_id: String,
+	pub id: String,
 }
 
-#[derive(Clone, Debug, Deserialize, Serialize)]
+#[derive(Clone, Debug, Deserialize)]
 pub struct NewCommentQuery {
-	pub author: String,
-	pub email: String,
-	pub text: String,
+	#[serde(flatten)]
+	pub comment: CommentForm,
 }
 
 #[derive(Clone, Debug, Deserialize)]
 pub struct EditCommentQuery {
-	pub author: String,
-	pub comment_id: String,
-	pub email: String,
-	pub text: String,
-	pub token: String,
+	#[serde(flatten)]
+	pub comment: CommentForm,
+	pub id: String,
+	//pub token: String,
 }
 
-#[derive(Clone, Debug, Deserialize)]
+/*#[derive(Clone, Debug, Deserialize)]
 pub struct RmCommentQuery {
-	pub comment_id: String,
-	pub token: String,
-}
+	pub id: String,
+	//pub token: String,
+}*/
 
 #[derive(Clone, Debug, Deserialize)]
 #[serde(tag = "a")]
@@ -47,8 +45,8 @@ pub enum CommentQuery {
 	NewComment(NewCommentQuery),
 	#[serde(rename = "edit_comment")]
 	EditComment(EditCommentQuery),
-	#[serde(rename = "rm_comment")]
-	RmComment(RmCommentQuery),
+	/*#[serde(rename = "rm_comment")]
+	RmComment(RmCommentQuery),*/
 }
 
 #[derive(Clone, Debug, Deserialize)]
@@ -71,3 +69,15 @@ pub struct ApproveQuery {
 pub struct RemoveQuery {
 	pub remove: String,
 }
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct EditQuery {
+	pub edit: String,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct CommentForm {
+	pub author: String,
+	pub email: String,
+	pub text: String,
+}
diff --git a/src/server.rs b/src/server.rs
index 189bb62..43b9232 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -23,8 +23,8 @@ pub async fn run_server(config: Arc<Config>, dbs: Dbs, templates: Arc<Templates>
 				config.clone(),
 				templates.clone(),
 				dbs.clone(),
-				&[],
 				Context::new(),
+				200,
 			)
 		}
 	});
@@ -63,8 +63,8 @@ async fn serve_comments<'a>(
 	config: Arc<Config>,
 	templates: Arc<Templates>,
 	dbs: Dbs,
-	errors: &[String],
 	mut context: Context,
+	status_code: u16,
 ) -> tide::Result<tide::Response> {
 	let Ok(topic) = req.param("topic") else {
 		return Err(tide::Error::from_str(404, "No topic"))
@@ -76,10 +76,8 @@ async fn serve_comments<'a>(
 
 	let topic_hash = TopicHash::from_topic(topic);
 
-	//let mut context = Context::new();
 	context.insert("config", &config);
 	context.insert("admin", &admin);
-	context.insert("new_comment_errors", errors);
 
 	if admin {
 		if let Ok(query) = req.query::<ApproveQuery>() {
@@ -91,17 +89,28 @@ async fn serve_comments<'a>(
 		}
 		if let Ok(query) = req.query::<RemoveQuery>() {
 			if let Ok(comment_id) = CommentId::from_base64(&query.remove) {
-				helpers::remove_pending_comment(comment_id, &dbs)
+				helpers::remove_comment(comment_id, &dbs)
 					.map_err(|e| error!("Removing comment: {:?}", e))
 					.ok();
 			}
 		}
+		if let Ok(query) = req.query::<EditQuery>() {
+			if let Ok(comment_id) = CommentId::from_base64(&query.edit) {
+				if let Some(comment) = dbs.comment.get(&comment_id).unwrap() {
+					context.insert("edit_comment", &comment_id.to_base64());
+					context.insert("edit_comment_author", &comment.author);
+					context.insert("edit_comment_email", &comment.email);
+					context.insert("edit_comment_text", &comment.text);
+				}
+			}
+		}
 
 		context.insert(
 			"comments_pending",
 			&helpers::iter_pending_comments_by_topic(topic_hash.clone(), &dbs)
 				.map(|(comment_id, comment)| CommentWithId {
 					author: comment.author,
+					editable: admin,
 					id: comment_id.to_base64(),
 					needs_approval: true,
 					post_time: comment.post_time,
@@ -116,6 +125,7 @@ async fn serve_comments<'a>(
 		&helpers::iter_approved_comments_by_topic(topic_hash, &dbs)
 			.map(|(comment_id, comment)| CommentWithId {
 				author: comment.author,
+				editable: admin,
 				id: comment_id.to_base64(),
 				needs_approval: false,
 				post_time: comment.post_time,
@@ -124,12 +134,10 @@ async fn serve_comments<'a>(
 			.collect::<Vec<CommentWithId>>(),
 	);
 
-	Ok(
-		tide::Response::builder(if errors.is_empty() { 200 } else { 400 })
-			.content_type(tide::http::mime::HTML)
-			.body(templates.tera.render("comments.html", &context)?)
-			.build(),
-	)
+	Ok(tide::Response::builder(status_code)
+		.content_type(tide::http::mime::HTML)
+		.body(templates.tera.render("comments.html", &context)?)
+		.build())
 }
 
 async fn serve_admin<'a>(
@@ -161,6 +169,7 @@ async fn serve_admin<'a>(
 					})?;
 				Some(CommentWithId {
 					author: comment.author,
+					editable: true,
 					id: comment_id.to_base64(),
 					needs_approval: true,
 					post_time: comment.post_time,
@@ -197,7 +206,11 @@ async fn handle_post_comments(
 	dbs: Dbs,
 	notify_send: Sender<()>,
 ) -> tide::Result<tide::Response> {
-	let client_addr = if config.antispam_enable {
+	let admin = req.cookie("admin").map_or(false, |psw| {
+		check_admin_password_hash(&config, &String::from(psw.value()))
+	});
+
+	let client_addr = if !admin && config.antispam_enable {
 		match helpers::get_client_addr(&config, &req) {
 			Some(Ok(addr)) => {
 				if config.antispam_whitelist.contains(&addr) {
@@ -228,27 +241,8 @@ async fn handle_post_comments(
 				return Err(tide::Error::from_str(404, "No topic"))
 			};
 
-			if query.author.len() > config.comment_author_max_len {
-				errors.push(format!(
-					"Author name length is {} but maximum is {}.",
-					query.author.len(),
-					config.comment_author_max_len
-				));
-			}
-			if query.email.len() > config.comment_email_max_len {
-				errors.push(format!(
-					"E-mail length is {} but maximum is {}.",
-					query.email.len(),
-					config.comment_email_max_len
-				));
-			}
-			if query.text.len() > config.comment_text_max_len {
-				errors.push(format!(
-					"Comment length is {} but maximum is {}.",
-					query.text.len(),
-					config.comment_text_max_len
-				));
-			}
+			helpers::check_comment(&config, &query.comment, &mut errors);
+
 			if let Some(client_addr) = &client_addr {
 				if let Some(antispam_timeout) =
 					helpers::antispam_check_client_mutation(client_addr, &dbs, &config).unwrap()
@@ -274,29 +268,95 @@ async fn handle_post_comments(
 
 				let comment = Comment {
 					topic_hash,
-					author: query.author,
-					email: if query.email.is_empty() {
+					author: query.comment.author,
+					email: if query.comment.email.is_empty() {
 						None
 					} else {
-						Some(query.email)
+						Some(query.comment.email)
 					},
 					last_edit_time: None,
 					post_time: time,
-					text: query.text,
+					text: query.comment.text,
 				};
 				helpers::new_pending_comment(&comment, &dbs)
 					.map_err(|e| error!("Adding pending comment: {:?}", e))
 					.ok();
 				notify_send.send(()).ok();
 			} else {
-				context.insert("new_comment_author", &query.author);
-				context.insert("new_comment_email", &query.email);
-				context.insert("new_comment_text", &query.text);
+				context.insert("new_comment_author", &query.comment.author);
+				context.insert("new_comment_email", &query.comment.email);
+				context.insert("new_comment_text", &query.comment.text);
 			}
+			context.insert("new_comment_errors", &errors);
+		}
+		CommentQuery::EditComment(query) => {
+			if !admin {
+				return Err(tide::Error::from_str(403, "Forbidden"));
+			}
+
+			helpers::check_comment(&config, &query.comment, &mut errors);
+
+			let comment_id = if let Ok(comment_id) = CommentId::from_base64(&query.id) {
+				comment_id
+			} else {
+				return Err(tide::Error::from_str(400, "Invalid comment id"));
+			};
+
+			let mut comment = if let Some(comment) = dbs.comment.get(&comment_id).unwrap() {
+				comment
+			} else {
+				return Err(tide::Error::from_str(404, "Not found"));
+			};
+
+			if let Some(client_addr) = &client_addr {
+				if let Some(antispam_timeout) =
+					helpers::antispam_check_client_mutation(client_addr, &dbs, &config).unwrap()
+				{
+					errors.push(format!(
+						"The edition quota from your IP is reached. You will be unblocked in {}s.",
+						antispam_timeout
+					));
+				}
+			}
+
+			if errors.is_empty() {
+				if let Some(client_addr) = &client_addr {
+					helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap();
+				}
+
+				let time = std::time::SystemTime::now()
+					.duration_since(std::time::UNIX_EPOCH)
+					.unwrap()
+					.as_secs();
+
+				comment.author = query.comment.author;
+				comment.email = if query.comment.email.is_empty() {
+					None
+				} else {
+					Some(query.comment.email)
+				};
+				comment.text = query.comment.text;
+				comment.last_edit_time = Some(time);
+
+				dbs.comment.insert(&comment_id, &comment).unwrap();
+			} else {
+				context.insert("edit_comment", &comment_id.to_base64());
+				context.insert("edit_comment_author", &query.comment.author);
+				context.insert("edit_comment_email", &query.comment.email);
+				context.insert("edit_comment_text", &query.comment.text);
+			}
+			context.insert("edit_comment_errors", &errors);
 		}
-		_ => {}
 	}
-	serve_comments(req, config, templates, dbs, &errors, context).await
+	serve_comments(
+		req,
+		config,
+		templates,
+		dbs,
+		context,
+		if errors.is_empty() { 200 } else { 400 },
+	)
+	.await
 }
 
 async fn handle_post_admin(
@@ -307,6 +367,7 @@ async fn handle_post_admin(
 ) -> tide::Result<tide::Response> {
 	if let Some(psw) = req.cookie("admin") {
 		if check_admin_password(&config, &String::from(psw.value())).is_some() {
+			#[allow(clippy::match_single_binding)]
 			match req.body_form::<AdminQuery>().await? {
 				_ => serve_admin(req, config, templates, dbs).await,
 			}
diff --git a/src/templates.rs b/src/templates.rs
index d78a64f..bb9e827 100644
--- a/src/templates.rs
+++ b/src/templates.rs
@@ -38,6 +38,7 @@ impl Templates {
 #[derive(Clone, Debug, Serialize)]
 pub struct CommentWithId {
 	pub author: String,
+	pub editable: bool,
 	pub id: String,
 	pub needs_approval: bool,
 	pub post_time: Time,
diff --git a/templates/comments.html b/templates/comments.html
index d9757a1..70a720b 100644
--- a/templates/comments.html
+++ b/templates/comments.html
@@ -8,14 +8,17 @@
 		{% if comments_pending %}
 		<div id="comments_pending">
 			{% for comment in comments_pending %}
-				<div class="comment{% if comment.needs_approval %} comment_pending{% endif %}" id="comment-{{ comment.id }}">
+				<div class="comment{% if comment.needs_approval %} comment_pending{% endif %}" id="comment-{{ comment.id | safe }}">
 					<span class="comment-author">{{ comment.author }}</span>
 					<span class="comment-date">{{ comment.post_time | date(format="%F %R", locale=config.lang) }}</span>
+					{% if comment.editable %}
+					<a href="?edit={{ comment.id | safe }}#edit_comment-form">Edit</a>
+					{% endif %}
 					{% if admin and comment.needs_approval %}
-					<a href="?approve={{ comment.id }}">Approve</a>
+					<a href="?approve={{ comment.id | safe }}">Approve</a>
 					{% endif %}
 					{% if admin %}
-					<a href="?remove={{ comment.id }}">Remove</a>
+					<a href="?remove={{ comment.id | safe }}">Remove</a>
 					{% endif %}
 					<p class="comment-text">{{ comment.text }}</p>
 				</div>
@@ -24,14 +27,17 @@
 		{% endif %}
 		<div id="comments">
 			{% for comment in comments %}
-				<div class="comment{% if comment.needs_approval %} comment_pending{% endif %}" id="comment-{{ comment.id }}">
+				<div class="comment{% if comment.needs_approval %} comment_pending{% endif %}" id="comment-{{ comment.id | safe }}">
 					<span class="comment-author">{{ comment.author }}</span>
 					<span class="comment-date">{{ comment.post_time | date(format="%F %R", locale=config.lang) }}</span>
+					{% if comment.editable %}
+					<a href="?edit={{ comment.id | safe }}#edit_comment-form">Edit</a>
+					{% endif %}
 					{% if admin and comment.needs_approval %}
-					<a href="?approve={{ comment.id }}">Approve</a>
+					<a href="?approve={{ comment.id | safe }}">Approve</a>
 					{% endif %}
 					{% if admin %}
-					<a href="?remove={{ comment.id }}">Remove</a>
+					<a href="?remove={{ comment.id | safe }}">Remove</a>
 					{% endif %}
 					<p class="comment-text">{{ comment.text }}</p>
 				</div>
@@ -47,12 +53,32 @@
 			</ul>
 			{% endif %}
 			<label for="new_comment-author">Your name:</label>
-			<input type="text" id="new_comment-author" name="author" maxlength="{{ config.comment_author_max_len }}"{% if new_comment_author %} value="{{ new_comment_author }}"{% endif %}/><br/>
+			<input type="text" id="new_comment-author" name="author" maxlength="{{ config.comment_author_max_len | safe }}"{% if new_comment_author %} value="{{ new_comment_author }}"{% endif %}/><br/>
 			<label for="new_comment-email">Your e-mail:</label>
-			<input type="email" id="new_comment-email" name="email" maxlength="{{ config.comment_email_max_len }}"{% if new_comment_email %} value="{{ new_comment_email }}"{% endif %}/><br/>
+			<input type="email" id="new_comment-email" name="email" maxlength="{{ config.comment_email_max_len | safe }}"{% if new_comment_email %} value="{{ new_comment_email }}"{% endif %}/><br/>
 			<label for="new_comment-text">Your comment:</label><br/>
-			<textarea id="new_comment-text" name="text" maxlength="{{ config.comment_text_max_len }}">{% if new_comment_text %}{{ new_comment_text }}{% endif %}</textarea><br/>
+			<textarea id="new_comment-text" name="text" maxlength="{{ config.comment_text_max_len | safe }}">{% if new_comment_text %}{{ new_comment_text }}{% endif %}</textarea><br/>
 			<button type="submit" name="a" value="new_comment">Post comment</button>
 		</form>
+		{% if edit_comment %}
+		<form id="edit_comment-form" action="#edit_comment-form" method="post">
+			{% if edit_comment_errors %}
+			<p>Whoops, the following error occurred:</p>
+			<ul id="edit_comment-errors" class="errors">
+				{% for error in edit_comment_errors %}
+				<li class="error">{{ error | safe }}</li>
+				{% endfor %}
+			</ul>
+			{% endif %}
+			<input type="hidden" name="id" value="{{ edit_comment | safe }}" autocomplete="off"/>
+			<label for="edit_comment-author">Your name:</label>
+			<input type="text" id="edit_comment-author" name="author" maxlength="{{ config.comment_author_max_len | safe }}"{% if edit_comment_author %} value="{{ edit_comment_author }}"{% endif %}/><br/>
+			<label for="edit_comment-email">Your e-mail:</label>
+			<input type="email" id="edit_comment-email" name="email" maxlength="{{ config.comment_email_max_len | safe }}"{% if edit_comment_email %} value="{{ edit_comment_email }}"{% endif %}/><br/>
+			<label for="edit_comment-text">Your comment:</label><br/>
+			<textarea id="edit_comment-text" name="text" maxlength="{{ config.comment_text_max_len | safe }}">{% if edit_comment_text %}{{ edit_comment_text }}{% endif %}</textarea><br/>
+			<button type="submit" name="a" value="edit_comment">Edit comment</button>
+		</form>
+		{% endif %}
 	</body>
 </html>