diff --git a/.gitignore b/.gitignore
index 8fdb9ad..6f848ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,3 +42,7 @@ minio_data/
/dumpster-server
keygen
steamfree
+
+# Tauri Android keystore (contains signing passwords)
+web/src-tauri/gen/android/keystore.properties
+
diff --git a/Makefile b/Makefile
index a56c97a..64576f6 100644
--- a/Makefile
+++ b/Makefile
@@ -18,6 +18,16 @@ build-tui:
build-tauri:
cd web && NODE_ENV=development NO_STRIP=true npx tauri build --bundles appimage,deb,rpm
+# Build signed Android APK
+build-android:
+ cd web && ANDROID_HOME=$(HOME)/Android/Sdk NDK_HOME=$(HOME)/Android/Sdk/ndk/27.2.12479018 JAVA_HOME=/usr/lib/jvm/java-17-openjdk npx tauri android build --apk
+ @echo "APK: web/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk"
+
+# Build signed Android AAB (Google Play bundle)
+build-android-aab:
+ cd web && ANDROID_HOME=$(HOME)/Android/Sdk NDK_HOME=$(HOME)/Android/Sdk/ndk/27.2.12479018 JAVA_HOME=/usr/lib/jvm/java-17-openjdk npx tauri android build --aab
+ @echo "AAB: web/src-tauri/gen/android/app/build/outputs/bundle/universalRelease/app-universal-release.aab"
+
# Generate Swagger docs
docs:
~/go/bin/swag init -g cmd/server/main.go -o docs
diff --git a/_hashgen.go b/_hashgen.go
new file mode 100644
index 0000000..73d250c
--- /dev/null
+++ b/_hashgen.go
@@ -0,0 +1,17 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/auth"
+)
+
+func main() {
+ hash, err := auth.HashPassword("password")
+ if err != nil {
+ panic(err)
+ }
+ fmt.Print(hash)
+}
diff --git a/cmd/server/main.go b/cmd/server/main.go
index cd883f5..b2968ca 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -428,6 +428,13 @@ func main() {
))
})
+ // Privacy policy page (served before SPA catch-all)
+ r.HandleFunc("/privacy", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Header().Set("Cache-Control", "public, max-age=3600")
+ w.Write([]byte(privacyPage))
+ })
+
// Static file serving for production (SPA)
staticDir := "web/dist"
if _, err := os.Stat(staticDir); err == nil {
@@ -450,3 +457,104 @@ func main() {
os.Exit(1)
}
}
+
+const privacyPage = `
+
+
+
+Privacy Policy
+dumpsterChat — Last updated: July 17, 2026
+
+Overview
+dumpsterChat is a self-hosted messaging platform. This privacy policy describes how your data is handled when you use the app. Because dumpsterChat is self-hosted, your data is stored on the server instance you connect to, which is operated by the server owner — not by us.
+
+Data We Collect
+When you use dumpsterChat, the following data is stored on the server:
+
+ - Account information: username, email address, avatar, and password hash (Argon2id, not reversible).
+ - Messages and content: text messages, reactions, uploaded files, voice activity metadata, and poll votes.
+ - Session data: login sessions stored in encrypted cookies.
+
+
+How We Use Your Data
+Your data is used solely to operate the chat platform:
+
+ - Deliver messages and notifications to the intended recipients.
+ - Sync read states and presence (online/offline) across your devices.
+ - Provide moderation tools (kicks, bans, mutes) per server rules.
+
+
+No Third-Party Analytics
+dumpsterChat does not include any analytics SDKs, tracking pixels, or telemetry. No usage data is sent to us or to any third party for advertising, profiling, or analytics purposes.
+
+Third-Party Integrations
+If enabled by the server owner, optional integrations may be used:
+
+ - Giphy: GIF search queries are proxied through the server to Giphy's API. No user data is shared with Giphy.
+ - LiveKit: Voice and video calls use a self-hosted LiveKit server. Media streams are processed in real time and are not recorded or stored by default.
+
+These integrations are optional and controlled entirely by the server owner.
+
+Data Retention
+Data is retained for as long as the server owner maintains the database. You can delete your messages or account at any time through the app. Server owners may also set message retention limits. Uploaded files persist until explicitly removed.
+
+Your Rights
+Depending on your jurisdiction, you may have the right to:
+
+ - Request a copy of your stored data.
+ - Delete your account and associated data.
+ - Correct inaccurate personal information.
+
+To exercise these rights, contact the operator of the dumpsterChat instance you use, or use the account management tools available within the app.
+
+Account Deletion Requests
+To request deletion of your account and all associated data:
+
+ - Use the Delete Account option in your account settings within the app.
+ - Or email account-deletion@dustin.coffee from the email address associated with your account.
+
+We will process your request within 30 days. Deletion removes your account, messages, uploaded files, and all personal data from the server.
+
+Security
+We take reasonable measures to protect your data:
+
+ - Passwords are hashed with Argon2id.
+ - Session tokens use httpOnly cookies.
+ - All communications are encrypted over HTTPS and WSS where available.
+
+
+Children's Privacy
+dumpsterChat is not directed at children under 13. We do not knowingly collect personal information from children.
+
+Changes to This Policy
+We may update this privacy policy from time to time. Changes will be posted at this URL. Continued use of the app after changes constitutes acceptance of the updated policy.
+
+Contact
+If you have questions about this privacy policy, contact the operator of the dumpsterChat instance you use, or open an issue on our project repository.
+
+This privacy policy applies to the dumpsterChat mobile app and web app. The specific data practices of each instance may vary based on the server operator's configuration.
+
+
+`
diff --git a/internal/auth/handlers.go b/internal/auth/handlers.go
index cc2c314..973dbd8 100644
--- a/internal/auth/handlers.go
+++ b/internal/auth/handlers.go
@@ -50,6 +50,7 @@ func (h *Handler) RegisterProtectedRoutes(r chi.Router) {
r.Get("/auth/me", h.Me)
r.Patch("/auth/me", h.UpdateProfile)
r.Put("/auth/me/password", h.ChangePassword)
+ r.Delete("/auth/me", h.DeleteAccount)
r.Get("/users/{userID}/profile", h.GetPublicProfile)
r.Get("/users/me/blocks", h.ListBlocks)
r.Post("/users/me/blocks", h.BlockUser)
@@ -662,3 +663,32 @@ func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "password updated"})
}
+
+// DeleteAccount deletes the authenticated user and all associated data.
+// ponytail: cascading FK handles all DB cleanup — push subscriptions cleaned manually.
+func (h *Handler) DeleteAccount(w http.ResponseWriter, r *http.Request) {
+ userID, ok := middleware.UserIDFromContext(r.Context())
+ if !ok {
+ http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
+ return
+ }
+
+ // Clean up push subscriptions
+ h.db.ExecContext(r.Context(), `DELETE FROM push_subscriptions WHERE user_id = $1`, userID)
+
+ // ON DELETE CASCADE handles everything else: sessions, tokens, messages,
+ // reactions, memberships, invites, bots, etc.
+ result, err := h.db.ExecContext(r.Context(), `DELETE FROM users WHERE id = $1`, userID)
+ if err != nil {
+ http.Error(w, `{"error":"failed to delete account"}`, http.StatusInternalServerError)
+ return
+ }
+ rows, _ := result.RowsAffected()
+ if rows == 0 {
+ http.Error(w, `{"error":"user not found"}`, http.StatusNotFound)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"message": "account deleted"})
+}
diff --git a/web/src-tauri/gen/android/.editorconfig b/web/src-tauri/gen/android/.editorconfig
new file mode 100644
index 0000000..ebe51d3
--- /dev/null
+++ b/web/src-tauri/gen/android/.editorconfig
@@ -0,0 +1,12 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = false
+insert_final_newline = false
\ No newline at end of file
diff --git a/web/src-tauri/gen/android/.gitignore b/web/src-tauri/gen/android/.gitignore
new file mode 100644
index 0000000..1c636c3
--- /dev/null
+++ b/web/src-tauri/gen/android/.gitignore
@@ -0,0 +1,20 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+key.properties
+keystore.properties
+
+/.tauri
+/tauri.settings.gradle
\ No newline at end of file
diff --git a/web/src-tauri/gen/android/app/.gitignore b/web/src-tauri/gen/android/app/.gitignore
new file mode 100644
index 0000000..6c4d56b
--- /dev/null
+++ b/web/src-tauri/gen/android/app/.gitignore
@@ -0,0 +1,6 @@
+/src/main/**/generated
+/src/main/jniLibs/**/*.so
+/src/main/assets/tauri.conf.json
+/tauri.build.gradle.kts
+/proguard-tauri.pro
+/tauri.properties
\ No newline at end of file
diff --git a/web/src-tauri/gen/android/app/build.gradle.kts b/web/src-tauri/gen/android/app/build.gradle.kts
new file mode 100644
index 0000000..dc5608c
--- /dev/null
+++ b/web/src-tauri/gen/android/app/build.gradle.kts
@@ -0,0 +1,90 @@
+import java.util.Properties
+
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("rust")
+}
+
+val tauriProperties = Properties().apply {
+ val propFile = file("tauri.properties")
+ if (propFile.exists()) {
+ propFile.inputStream().use { load(it) }
+ }
+}
+
+val keystorePropertiesFile = rootProject.file("keystore.properties")
+val keystoreProperties = Properties()
+if (keystorePropertiesFile.exists()) {
+ keystoreProperties.load(keystorePropertiesFile.inputStream())
+}
+
+android {
+ compileSdk = 36
+ namespace = "coffee.dustin.dumpster"
+ defaultConfig {
+ manifestPlaceholders["usesCleartextTraffic"] = "false"
+ applicationId = "coffee.dustin.dumpster"
+ minSdk = 24
+ targetSdk = 36
+ versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
+ versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
+ }
+ signingConfigs {
+ if (keystorePropertiesFile.exists()) {
+ create("release") {
+ storeFile = file(keystoreProperties["storeFile"] as String)
+ storePassword = keystoreProperties["storePassword"] as String
+ keyAlias = keystoreProperties["keyAlias"] as String
+ keyPassword = keystoreProperties["keyPassword"] as String
+ }
+ }
+ }
+ buildTypes {
+ getByName("debug") {
+ manifestPlaceholders["usesCleartextTraffic"] = "true"
+ isDebuggable = true
+ isJniDebuggable = true
+ isMinifyEnabled = false
+ packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
+ jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
+ jniLibs.keepDebugSymbols.add("*/x86/*.so")
+ jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
+ }
+ }
+ getByName("release") {
+ if (keystorePropertiesFile.exists()) {
+ signingConfig = signingConfigs.getByName("release")
+ }
+ isMinifyEnabled = true
+ proguardFiles(
+ *fileTree(".") { include("**/*.pro") }
+ .plus(getDefaultProguardFile("proguard-android-optimize.txt"))
+ .toList().toTypedArray()
+ )
+ }
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ buildConfig = true
+ }
+}
+
+rust {
+ rootDirRel = "../../../"
+}
+
+dependencies {
+ implementation("androidx.webkit:webkit:1.14.0")
+ implementation("androidx.appcompat:appcompat:1.7.1")
+ implementation("androidx.activity:activity-ktx:1.10.1")
+ implementation("com.google.android.material:material:1.12.0")
+ implementation("androidx.lifecycle:lifecycle-process:2.10.0")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.4")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
+}
+
+apply(from = "tauri.build.gradle.kts")
\ No newline at end of file
diff --git a/web/src-tauri/gen/android/app/proguard-rules.pro b/web/src-tauri/gen/android/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/web/src-tauri/gen/android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/web/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/web/src-tauri/gen/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ea86fc6
--- /dev/null
+++ b/web/src-tauri/gen/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,37 @@
+
+