Index: src/routes/account.js
===================================================================
--- src/routes/account.js	(revision a885afc878cdd6df0962a173a88ddd8f69f820ec)
+++ src/routes/account.js	(revision 7c44c1227391a3e62327e785d8a67b661aaae489)
@@ -124,6 +124,26 @@
 
 // ==================== UPDATE BIO ====================
+const RESERVED_USERNAMES = new Set(['admin', 'account', 'auth', 'login', 'register', 'logout', 'user', 'users', 'api', 'fediverse', 'posts', 'media', 'audio', 'assets', 'cirkel', 'authorize_interaction']);
+
 router.post('/profile', requireAuth, (req, res) => {
   const bio = (req.body.bio || '').toString().slice(0, 500).trim();
+
+  // Username (login + display name; does NOT affect the fediverse handle, which
+  // is the site slug). Validate: format + reserved + unique (case-insensitive).
+  const username = (req.body.username || '').toString().trim();
+  if (username && username !== req.session.user.username) {
+    if (!/^[A-Za-z0-9_-]{2,30}$/.test(username)) {
+      return res.redirect('/account?error=' + encodeURIComponent('Gebruikersnaam: 2-30 tekens; letters, cijfers, _ en - .'));
+    }
+    if (RESERVED_USERNAMES.has(username.toLowerCase())) {
+      return res.redirect('/account?error=' + encodeURIComponent('Die gebruikersnaam is gereserveerd.'));
+    }
+    const uTaken = db.prepare('SELECT 1 FROM users WHERE LOWER(username) = LOWER(?) AND id != ?').get(username, req.session.user.id);
+    if (uTaken) {
+      return res.redirect('/account?error=' + encodeURIComponent('Die gebruikersnaam is al in gebruik.'));
+    }
+    db.prepare('UPDATE users SET username = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(username, req.session.user.id);
+    req.session.user.username = username;
+  }
 
   // Email (optionally also changed). Validation: valid format + not already in use
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision a885afc878cdd6df0962a173a88ddd8f69f820ec)
+++ src/services/i18n.js	(revision 7c44c1227391a3e62327e785d8a67b661aaae489)
@@ -799,4 +799,5 @@
     'acct.member_since': 'Lid sinds',
     'acct.avatar_remove': 'Foto verwijderen',
+    'acct.username': 'Gebruikersnaam',
     'acct.email': 'E-mailadres',
     'acct.email_ph': 'jij@email.nl',
@@ -1792,4 +1793,5 @@
     'acct.member_since': 'Member since',
     'acct.avatar_remove': 'Remove photo',
+    'acct.username': 'Username',
     'acct.email': 'Email address',
     'acct.email_ph': 'you@email.com',
@@ -2785,4 +2787,5 @@
     'acct.member_since': 'Mitglied seit',
     'acct.avatar_remove': 'Foto entfernen',
+    'acct.username': 'Benutzername',
     'acct.email': 'E-Mail-Adresse',
     'acct.email_ph': 'du@email.de',
Index: src/views/pages/account.ejs
===================================================================
--- src/views/pages/account.ejs	(revision a885afc878cdd6df0962a173a88ddd8f69f820ec)
+++ src/views/pages/account.ejs	(revision 7c44c1227391a3e62327e785d8a67b661aaae489)
@@ -71,4 +71,8 @@
       <form action="/account/profile" method="post" class="ax-form ax-form-bio">
         <label class="ax-field">
+          <span><%= t('acct.username') %></span>
+          <input type="text" name="username" value="<%= account.username %>" minlength="2" maxlength="30" pattern="[A-Za-z0-9_\-]{2,30}" autocomplete="username">
+        </label>
+        <label class="ax-field">
           <span><%= t('acct.email') %></span>
           <input type="email" name="email" value="<%= account.email || '' %>" maxlength="254" autocomplete="email" placeholder="<%= t('acct.email_ph') %>">
