diff --git a/API/ROLAC.API/Entities/AppRole.cs b/API/ROLAC.API/Entities/AppRole.cs
new file mode 100644
index 0000000..08a9cc0
--- /dev/null
+++ b/API/ROLAC.API/Entities/AppRole.cs
@@ -0,0 +1,8 @@
+using Microsoft.AspNetCore.Identity;
+
+namespace ROLAC.API.Entities;
+
+public class AppRole : IdentityRole
+{
+ public string? Description { get; set; }
+}
diff --git a/API/ROLAC.API/Entities/AppUser.cs b/API/ROLAC.API/Entities/AppUser.cs
new file mode 100644
index 0000000..c0493d4
--- /dev/null
+++ b/API/ROLAC.API/Entities/AppUser.cs
@@ -0,0 +1,21 @@
+using Microsoft.AspNetCore.Identity;
+
+namespace ROLAC.API.Entities;
+
+public class AppUser : IdentityUser
+{
+ /// Links this login account to a church member record. Null for admin-only accounts.
+ public int? MemberId { get; set; }
+
+ /// UI language preference: 'en' or 'zh-TW'.
+ public string LanguagePreference { get; set; } = "en";
+
+ /// False = account suspended (returns 403 even with correct password).
+ public bool IsActive { get; set; } = true;
+
+ public DateTime? LastLoginAt { get; set; }
+
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+
+ public ICollection RefreshTokens { get; set; } = new List();
+}
diff --git a/API/ROLAC.API/Entities/RefreshToken.cs b/API/ROLAC.API/Entities/RefreshToken.cs
new file mode 100644
index 0000000..328e3f9
--- /dev/null
+++ b/API/ROLAC.API/Entities/RefreshToken.cs
@@ -0,0 +1,29 @@
+namespace ROLAC.API.Entities;
+
+public class RefreshToken
+{
+ public int Id { get; set; }
+
+ public string UserId { get; set; } = null!;
+ public AppUser User { get; set; } = null!;
+
+ /// SHA-256 hex of the raw token sent to the client. Never store raw tokens.
+ public string TokenHash { get; set; } = null!;
+
+ public DateTime ExpiresAt { get; set; }
+ public DateTime CreatedAt { get; set; }
+
+ /// Set when this token is revoked (logout or rotation).
+ public DateTime? RevokedAt { get; set; }
+
+ /// Points to the hash of the token that replaced this one during rotation.
+ public string? ReplacedByHash { get; set; }
+
+ public string? DeviceInfo { get; set; }
+ public string? IpAddress { get; set; }
+
+ // Computed helpers — NOT mapped to DB columns (ignored in OnModelCreating)
+ public bool IsExpired => DateTime.UtcNow >= ExpiresAt;
+ public bool IsRevoked => RevokedAt.HasValue;
+ public bool IsActive => !IsRevoked && !IsExpired;
+}