diff --git a/.env.example b/.env.example
index 5ece4e2..a2ef39b 100644
--- a/.env.example
+++ b/.env.example
@@ -1,7 +1,7 @@
EXPO_PUBLIC_USE_LOCAL_API=1
EXPO_PUBLIC_LOCAL_API_URL=http://192.168.76.129:8080
-EXPO_PUBLIC_API_URL=https://devbits.ddns.net
+EXPO_PUBLIC_API_URL=https://devbits.app
EXPO_PUBLIC_LOCAL_API_PORT=8080
POSTGRES_DB=devbits_dev
POSTGRES_USER=devbits_dev
-POSTGRES_PASSWORD=devbits_dev_password
\ No newline at end of file
+POSTGRES_PASSWORD=devbits_dev_password
diff --git a/Bash-Scripts/run-dev.sh b/Bash-Scripts/run-dev.sh
index cbf2078..1011e8b 100644
--- a/Bash-Scripts/run-dev.sh
+++ b/Bash-Scripts/run-dev.sh
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
# Script: run-dev.sh
-# Does: Boots local dev DB + local backend (isolated compose project), then launches frontend in local mode.
-# Use: ./run-dev.sh [--clear]
+# Does: Boots local dev DB + local backend (isolated compose project) only.
+# Use: ./run-dev.sh
# DB: devbits_dev (user/pass: devbits_dev/devbits_dev_password) in compose project devbits-dev-local.
# Ports: backend default :8080, DB default :5433 (DEVBITS_BACKEND_PORT / DEVBITS_DB_PORT override).
-# Modes: Frontend=ON(local API) | Backend=ON(local Docker) | Live stack untouched | Test DB untouched.
+# Modes: Frontend=OFF | Backend=ON(local Docker) | Live stack untouched | Test DB untouched.
set -euo pipefail
@@ -14,9 +14,9 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
BACKEND_DIR="$ROOT/backend"
COMPOSE_PROJECT="devbits-dev-local"
-CLEAR_FRONTEND=""
-if [[ "${1:-}" == "--clear" ]]; then
- CLEAR_FRONTEND="--clear"
+if [[ "${EUID}" -eq 0 ]]; then
+ echo "Warning: Running as root is not recommended for Expo local mode."
+ echo "Use ./run-dev.sh (without sudo) so LAN IP detection and device connectivity work reliably."
fi
if ! command -v docker >/dev/null 2>&1; then
@@ -109,6 +109,10 @@ for i in $(seq 1 60); do
sleep 1
done
-echo "Launching frontend in local backend mode..."
-cd "$ROOT"
-EXPO_PUBLIC_LOCAL_API_PORT="$DEVBITS_BACKEND_PORT" "$SCRIPT_DIR/run-front.sh" --local $CLEAR_FRONTEND
\ No newline at end of file
+echo
+echo "Local backend stack is ready."
+echo "Backend health: http://localhost:${DEVBITS_BACKEND_PORT}/health"
+echo "To launch frontend against local backend, run:"
+echo " EXPO_PUBLIC_LOCAL_API_PORT=${DEVBITS_BACKEND_PORT} $SCRIPT_DIR/run-front.sh --local"
+echo "To launch frontend against live backend, run:"
+echo " $SCRIPT_DIR/run-front.sh --live"
\ No newline at end of file
diff --git a/Bash-Scripts/run-front.sh b/Bash-Scripts/run-front.sh
index d3cab0e..e5c0e99 100644
--- a/Bash-Scripts/run-front.sh
+++ b/Bash-Scripts/run-front.sh
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Script: run-front.sh
-# Does: Starts Expo frontend and lets you choose backend target (Production or Local).
-# Use: ./run-front.sh [--local|--production] [--clear] [--dev-client]
+# Does: Starts Expo frontend and lets you choose backend target (Local or Live).
+# Use: ./run-front.sh [--local|--live|--production] [--clear] [--dev-client]
# DB: None (frontend only).
# Ports: Metro uses LAN IP; local API defaults to :8080 (EXPO_PUBLIC_LOCAL_API_PORT overrides).
# Modes: Frontend=ON | Backend=Production URL or Local URL | Live stack untouched | Dev/Test DB untouched.
@@ -31,26 +31,53 @@ for arg in "$@"; do
--production)
MODE="production"
;;
+ --live)
+ MODE="production"
+ ;;
*)
echo "Unknown argument: $arg"
- echo "Usage: ./run-front.sh [--local|--production] [--clear] [--dev-client]"
+ echo "Usage: ./run-front.sh [--local|--live|--production] [--clear] [--dev-client]"
exit 1
;;
esac
done
detect_lan_ip() {
- hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.' | head -n1
+ local from_route=""
+ local iface=""
+
+ if command -v ip >/dev/null 2>&1; then
+ from_route="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1; i<=NF; i++) if ($i=="src") {print $(i+1); exit}}')"
+ if [[ "$from_route" =~ ^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\. ]]; then
+ echo "$from_route"
+ return 0
+ fi
+
+ iface="$(ip route 2>/dev/null | awk '/^default/ {print $5; exit}')"
+ if [[ -n "$iface" ]]; then
+ ip -4 addr show dev "$iface" scope global 2>/dev/null |
+ awk '/inet / {print $2}' |
+ cut -d/ -f1 |
+ grep -E '^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.' |
+ head -n1
+ return 0
+ fi
+ fi
+
+ hostname -I 2>/dev/null |
+ tr ' ' '\n' |
+ grep -E '^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.' |
+ head -n1
}
LAN_IP="$(detect_lan_ip || true)"
if [[ -z "$LAN_IP" ]]; then
LAN_IP="127.0.0.1"
- echo "Warning: Could not detect private LAN IPv4. Falling back to 127.0.0.1."
+ echo "Warning: Could not detect private LAN IPv4."
fi
if [[ -z "$MODE" ]]; then
- echo "Select backend: 1) Production (devbits.ddns.net) 2) Local (LAN IP:8080)"
+ echo "Select backend: 1) Live (devbits.app) 2) Local (LAN IP:8080)"
read -r -p "Choose [1/2]: " selection
case "$selection" in
1) MODE="production" ;;
@@ -69,18 +96,30 @@ else
unset REACT_NATIVE_PACKAGER_HOSTNAME || true
unset EXPO_PACKAGER_HOSTNAME || true
fi
-export EXPO_PUBLIC_API_URL="https://devbits.ddns.net"
-export EXPO_PUBLIC_API_FALLBACK_URL="https://devbits.ddns.net"
+export EXPO_PUBLIC_API_URL="https://devbits.app"
+export EXPO_PUBLIC_API_FALLBACK_URL="https://devbits.app"
if [[ "$MODE" == "local" ]]; then
LOCAL_API_PORT="${EXPO_PUBLIC_LOCAL_API_PORT:-8080}"
+
+ if [[ "$LAN_IP" == "127.0.0.1" && "${DEVBITS_ALLOW_LOOPBACK_LOCAL_API:-0}" != "1" ]]; then
+ echo "Error: Local mode resolved loopback (127.0.0.1), which will fail on physical devices."
+ echo "Fix one of the following and try again:"
+ echo " 1) Run without sudo so network detection can read your user network context."
+ echo " 2) Set EXPO_PUBLIC_LOCAL_API_URL manually, e.g. http://192.168.x.y:${LOCAL_API_PORT}."
+ echo " 3) If using only simulator/emulator intentionally, set DEVBITS_ALLOW_LOOPBACK_LOCAL_API=1."
+ exit 1
+ fi
+
export EXPO_PUBLIC_USE_LOCAL_API=1
- export EXPO_PUBLIC_LOCAL_API_URL="http://${LAN_IP}:${LOCAL_API_PORT}"
+ if [[ -z "${EXPO_PUBLIC_LOCAL_API_URL:-}" ]]; then
+ export EXPO_PUBLIC_LOCAL_API_URL="http://${LAN_IP}:${LOCAL_API_PORT}"
+ fi
echo "Using local backend: $EXPO_PUBLIC_LOCAL_API_URL"
else
export EXPO_PUBLIC_USE_LOCAL_API=0
unset EXPO_PUBLIC_LOCAL_API_URL || true
- echo "Using production backend: https://devbits.ddns.net"
+ echo "Using live backend: https://devbits.app"
fi
cd "$FRONTEND_DIR"
diff --git a/Bash-Scripts/sync-static.sh b/Bash-Scripts/sync-static.sh
new file mode 100755
index 0000000..02df63d
--- /dev/null
+++ b/Bash-Scripts/sync-static.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+# Script: sync-static.sh
+# Does: Copies compliance/static files from backend/api/static (source of truth) to frontend/public.
+# Run this whenever the backend static files change to keep the frontend web build in sync.
+# Use: ./sync-static.sh
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+SRC="$ROOT/backend/api/static"
+DST="$ROOT/frontend/public"
+
+FILES=(
+ apple-app-site-association
+ privacy-policy.html
+ account-deletion.html
+)
+
+for file in "${FILES[@]}"; do
+ src_path="$SRC/$file"
+ dst_path="$DST/$file"
+
+ if [[ ! -f "$src_path" ]]; then
+ echo "WARNING: Source file not found, skipping: $src_path"
+ continue
+ fi
+
+ if cmp -s "$src_path" "$dst_path" 2>/dev/null; then
+ echo "Up to date: $file"
+ else
+ cp "$src_path" "$dst_path"
+ echo "Synced: $file"
+ fi
+done
+
+echo "Sync complete."
diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md
index dd41481..b0aa2aa 100644
--- a/INSTRUCTIONS.md
+++ b/INSTRUCTIONS.md
@@ -2,22 +2,50 @@
## Backend
-> **Live/Deployed Stack**
+> **AWS EC2 (Amazon Linux) Native Deploy**
>
> ```bash
-> cd /path/to/DevBits/backend
-> docker compose up -d
-> docker compose logs -f db
+> # On your EC2 instance
+> sudo dnf update -y
+> sudo dnf install -y git tar
> ```
-
-> Rebuild and restart:
+>
+> Install Go 1.24.x (required by `backend/go.mod`):
>
> ```bash
-> docker compose up -d --build
+> curl -LO https://go.dev/dl/go1.24.2.linux-amd64.tar.gz
+> sudo rm -rf /usr/local/go
+> sudo tar -C /usr/local -xzf go1.24.2.linux-amd64.tar.gz
+> echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
+> source ~/.bashrc
+> go version
> ```
-
+>
+> Clone and deploy:
+>
+> ```bash
+> sudo mkdir -p /opt/devbits
+> sudo chown -R "$USER":"$USER" /opt/devbits
+> cd /opt/devbits
+> git clone https://github.com/devbits-go/DevBits.git .
+> git checkout aws-ready-main
+> cd backend
+> cp .env.example .env
+> # edit .env with production values (DATABASE_URL, secrets, CORS, etc.)
+> ./scripts/deploy-aws-native.sh
+> ```
+>
+> Verify service:
+>
+> ```bash
+> sudo systemctl status devbits-api --no-pager
+> sudo journalctl -u devbits-api -n 120 --no-pager
+> curl -i http://127.0.0.1:8080/health
+> ```
+>
> [!TIP]
-> Check `backend/scripts/README.md` for database operations.
+> AWS deploy uses native `systemd` (no Docker or nginx required in production).
+> See `backend/docs/AWS_TRANSFER_NO_NGINX.md` for full runbook.
## Build
@@ -58,4 +86,4 @@
---
-`Workflow: Backend Setup → EAS Build → EAS Submit`
+`Workflow: AWS Backend Deploy → EAS Build → EAS Submit`
diff --git a/backend/scripts/backup-deployment-db.ps1 b/Old Scripts/backend/scripts/backup-deployment-db.ps1
similarity index 66%
rename from backend/scripts/backup-deployment-db.ps1
rename to Old Scripts/backend/scripts/backup-deployment-db.ps1
index 466a097..ee60895 100644
--- a/backend/scripts/backup-deployment-db.ps1
+++ b/Old Scripts/backend/scripts/backup-deployment-db.ps1
@@ -5,12 +5,6 @@ param(
$ErrorActionPreference = "Stop"
-if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
- $arguments = "& '" + $myinvocation.mycommand.definition + "'"
- Start-Process powershell -Verb runAs -ArgumentList $arguments
- exit
-}
-
Write-Host "Creating deployment database backup..." -ForegroundColor Yellow
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
@@ -21,23 +15,28 @@ try {
New-Item -ItemType Directory -Path $backupPath -Force | Out-Null
$envFile = Join-Path $root ".env"
- $dbUser = "devbits"
- $dbName = "devbits"
- if (Test-Path $envFile) {
- $envRaw = Get-Content -Path $envFile -Raw
- if ($envRaw -match "(?m)^POSTGRES_USER=(.+)$") {
- $dbUser = $Matches[1].Trim()
- }
- if ($envRaw -match "(?m)^POSTGRES_DB=(.+)$") {
- $dbName = $Matches[1].Trim()
- }
+ if (-not (Test-Path $envFile)) {
+ throw "Missing $envFile. Create it from backend/.env.example and set DATABASE_URL."
+ }
+
+ $envRaw = Get-Content -Path $envFile -Raw
+ $dbUrl = $null
+ if ($envRaw -match "(?m)^DATABASE_URL=(.+)$") {
+ $dbUrl = $Matches[1].Trim()
+ }
+ if ([string]::IsNullOrWhiteSpace($dbUrl)) {
+ throw "Missing DATABASE_URL in $envFile"
+ }
+
+ if (-not (Get-Command pg_dump -ErrorAction SilentlyContinue)) {
+ throw "pg_dump not found in PATH. Install PostgreSQL client tools first."
}
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$dbBackupFileName = "devbits-db-$timestamp.sql"
$dbBackupFile = Join-Path $backupPath $dbBackupFileName
-
- docker compose exec -T db pg_dump -U $dbUser -d $dbName --no-owner --no-privileges | Out-File -FilePath $dbBackupFile -Encoding utf8
+
+ pg_dump $dbUrl --no-owner --no-privileges | Out-File -FilePath $dbBackupFile -Encoding utf8
if (-not (Test-Path $dbBackupFile)) {
throw "Database backup file was not created."
@@ -69,20 +68,11 @@ finally {
Pop-Location
}
-$liveBackendState = "unavailable"
-try {
- $statusOutput = docker compose -f (Join-Path $root "docker-compose.yml") ps backend 2>$null
- if ($LASTEXITCODE -eq 0) {
- $liveBackendState = if (($statusOutput | Out-String) -match "Up") { "running" } else { "not running" }
- }
-}
-catch {}
-
Write-Host ""
Write-Host "===== Summary =====" -ForegroundColor Cyan
Write-Host "Action: Deployment backup created"
Write-Host "Updated: Latest DB + uploads backup files retained"
-Write-Host "Live backend: $liveBackendState"
+Write-Host "Database backup target: DATABASE_URL"
if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") {
Read-Host "Press Enter to close"
diff --git a/backend/scripts/disable-daily-backup-task.ps1 b/Old Scripts/backend/scripts/disable-daily-backup-task.ps1
similarity index 76%
rename from backend/scripts/disable-daily-backup-task.ps1
rename to Old Scripts/backend/scripts/disable-daily-backup-task.ps1
index 804b456..a86467e 100644
--- a/backend/scripts/disable-daily-backup-task.ps1
+++ b/Old Scripts/backend/scripts/disable-daily-backup-task.ps1
@@ -23,20 +23,12 @@ Write-Host "Task removed." -ForegroundColor Green
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$root = Resolve-Path (Join-Path $scriptDir "..")
-$liveBackendState = "unavailable"
-try {
- $statusOutput = docker compose -f (Join-Path $root "docker-compose.yml") ps backend 2>$null
- if ($LASTEXITCODE -eq 0) {
- $liveBackendState = if (($statusOutput | Out-String) -match "Up") { "running" } else { "not running" }
- }
-}
-catch {}
Write-Host ""
Write-Host "===== Summary =====" -ForegroundColor Cyan
Write-Host "Action: Daily backup task removed"
Write-Host "Updated: Windows scheduled task '$TaskName' deleted"
-Write-Host "Live backend: $liveBackendState"
+Write-Host "Execution target removed: backup-deployment-db.ps1"
if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") {
Read-Host "Press Enter to close"
diff --git a/backend/scripts/reset-deployment-db.ps1 b/Old Scripts/backend/scripts/reset-deployment-db.ps1
similarity index 51%
rename from backend/scripts/reset-deployment-db.ps1
rename to Old Scripts/backend/scripts/reset-deployment-db.ps1
index ee7acae..21dd784 100644
--- a/backend/scripts/reset-deployment-db.ps1
+++ b/Old Scripts/backend/scripts/reset-deployment-db.ps1
@@ -7,12 +7,6 @@ $ErrorActionPreference = "Stop"
Write-Host "Resetting DevBits deployment database to a blank slate..." -ForegroundColor Yellow
-if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
- $arguments = "& '" + $myinvocation.mycommand.definition + "'"
- Start-Process powershell -Verb runAs -ArgumentList $arguments
- exit
-}
-
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$root = Resolve-Path (Join-Path $scriptDir "..")
Push-Location $root
@@ -22,7 +16,28 @@ try {
throw "Missing $envFile. Create it from backend/.env.example and set strong credentials before resetting."
}
- docker compose down -v --remove-orphans
+ $envRaw = Get-Content -Path $envFile -Raw
+ $dbUrl = $null
+ if ($envRaw -match "(?m)^DATABASE_URL=(.+)$") {
+ $dbUrl = $Matches[1].Trim()
+ }
+ if ([string]::IsNullOrWhiteSpace($dbUrl)) {
+ throw "Missing DATABASE_URL in $envFile"
+ }
+
+ if (-not (Get-Command psql -ErrorAction SilentlyContinue)) {
+ throw "psql not found in PATH. Install PostgreSQL client tools first."
+ }
+
+ psql $dbUrl -v ON_ERROR_STOP=1 -c "DROP SCHEMA IF EXISTS public CASCADE;"
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed dropping public schema."
+ }
+
+ psql $dbUrl -v ON_ERROR_STOP=1 -c "CREATE SCHEMA public;"
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed creating public schema."
+ }
if (-not $KeepUploads) {
$uploadsPath = Join-Path $root "uploads"
@@ -31,7 +46,13 @@ try {
}
}
- docker compose up -d --build
+ $hasService = (Get-Command systemctl -ErrorAction SilentlyContinue)
+ if ($hasService) {
+ systemctl list-unit-files | Select-String -Pattern '^devbits-api\.service' | Out-Null
+ if ($LASTEXITCODE -eq 0) {
+ sudo systemctl restart devbits-api
+ }
+ }
Write-Host "Database reset complete. All users and app data are removed." -ForegroundColor Green
}
@@ -39,20 +60,11 @@ finally {
Pop-Location
}
-$liveBackendState = "unavailable"
-try {
- $statusOutput = docker compose -f (Join-Path $root "docker-compose.yml") ps backend 2>$null
- if ($LASTEXITCODE -eq 0) {
- $liveBackendState = if (($statusOutput | Out-String) -match "Up") { "running" } else { "not running" }
- }
-}
-catch {}
-
Write-Host ""
Write-Host "===== Summary =====" -ForegroundColor Cyan
Write-Host "Action: Deployment DB reset executed"
-Write-Host "Updated: Database recreated and services rebuilt"
-Write-Host "Live backend: $liveBackendState"
+Write-Host "Updated: Public schema recreated (blank state)"
+Write-Host "Database reset target: DATABASE_URL"
if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") {
Read-Host "Press Enter to close"
diff --git a/backend/scripts/restore-deployment-db.ps1 b/Old Scripts/backend/scripts/restore-deployment-db.ps1
similarity index 55%
rename from backend/scripts/restore-deployment-db.ps1
rename to Old Scripts/backend/scripts/restore-deployment-db.ps1
index 27e8bb9..93b55ad 100644
--- a/backend/scripts/restore-deployment-db.ps1
+++ b/Old Scripts/backend/scripts/restore-deployment-db.ps1
@@ -5,25 +5,6 @@ param(
$ErrorActionPreference = "Stop"
-if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
- $argumentList = @(
- "-NoProfile",
- "-ExecutionPolicy", "Bypass",
- "-NoExit",
- "-File", "`"$($MyInvocation.MyCommand.Path)`""
- )
-
- if ($PSBoundParameters.ContainsKey("BackupDir")) {
- $argumentList += @("-BackupDir", "`"$BackupDir`"")
- }
- if ($NoPause) {
- $argumentList += "-NoPause"
- }
-
- Start-Process powershell -Verb runAs -ArgumentList ($argumentList -join " ")
- exit
-}
-
Write-Host "Restoring deployment database from backup..." -ForegroundColor Yellow
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
@@ -51,38 +32,34 @@ try {
Write-Host "Using backup: $resolvedBackup" -ForegroundColor Cyan
$envFile = Join-Path $root ".env"
- $dbUser = "devbits"
- $dbName = "devbits"
- if (Test-Path $envFile) {
- $envRaw = Get-Content -Path $envFile -Raw
- if ($envRaw -match "(?m)^POSTGRES_USER=(.+)$") {
- $dbUser = $Matches[1].Trim()
- }
- if ($envRaw -match "(?m)^POSTGRES_DB=(.+)$") {
- $dbName = $Matches[1].Trim()
- }
+ if (-not (Test-Path $envFile)) {
+ throw "Missing $envFile. Create it from backend/.env.example and set DATABASE_URL."
}
- $terminateSql = "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$dbName' AND pid <> pg_backend_pid();"
- $dropDbSql = "DROP DATABASE IF EXISTS `"$dbName`;"
- $createDbSql = "CREATE DATABASE `"$dbName`;"
+ $envRaw = Get-Content -Path $envFile -Raw
+ $dbUrl = $null
+ if ($envRaw -match "(?m)^DATABASE_URL=(.+)$") {
+ $dbUrl = $Matches[1].Trim()
+ }
+ if ([string]::IsNullOrWhiteSpace($dbUrl)) {
+ throw "Missing DATABASE_URL in $envFile"
+ }
- docker compose exec -T db psql -U $dbUser -d postgres -c $terminateSql
- if ($LASTEXITCODE -ne 0) {
- throw "Failed terminating active connections."
+ if (-not (Get-Command psql -ErrorAction SilentlyContinue)) {
+ throw "psql not found in PATH. Install PostgreSQL client tools first."
}
- docker compose exec -T db psql -U $dbUser -d postgres -c $dropDbSql
+ psql $dbUrl -v ON_ERROR_STOP=1 -c "DROP SCHEMA IF EXISTS public CASCADE;"
if ($LASTEXITCODE -ne 0) {
- throw "Failed dropping existing database."
+ throw "Failed dropping public schema."
}
- docker compose exec -T db psql -U $dbUser -d postgres -c $createDbSql
+ psql $dbUrl -v ON_ERROR_STOP=1 -c "CREATE SCHEMA public;"
if ($LASTEXITCODE -ne 0) {
- throw "Failed creating target database."
+ throw "Failed creating public schema."
}
- Get-Content -Path $resolvedBackup -Raw | docker compose exec -T db psql -U $dbUser -d $dbName
+ Get-Content -Path $resolvedBackup -Raw | psql $dbUrl -v ON_ERROR_STOP=1
if ($LASTEXITCODE -ne 0) {
throw "Restore failed while applying SQL backup."
}
@@ -111,14 +88,15 @@ try {
Write-Host "No matching uploads backup found for timestamp $dbTimestamp, keeping current uploads directory." -ForegroundColor Yellow
}
- Write-Host "Restore complete. Rebuilding deployment services..." -ForegroundColor Yellow
-
- docker compose up -d --build
- if ($LASTEXITCODE -ne 0) {
- throw "Restore completed, but service rebuild failed."
+ $hasService = (Get-Command systemctl -ErrorAction SilentlyContinue)
+ if ($hasService) {
+ systemctl list-unit-files | Select-String -Pattern '^devbits-api\.service' | Out-Null
+ if ($LASTEXITCODE -eq 0) {
+ sudo systemctl restart devbits-api
+ }
}
- Write-Host "Restore complete and services rebuilt." -ForegroundColor Green
+ Write-Host "Restore complete." -ForegroundColor Green
}
finally {
Pop-Location
@@ -130,20 +108,11 @@ catch {
}
finally {
$operationState = if ($failed) { "failed" } else { "success" }
- $liveBackendState = "unavailable"
- try {
- $statusOutput = docker compose -f (Join-Path $root "docker-compose.yml") ps backend 2>$null
- if ($LASTEXITCODE -eq 0) {
- $liveBackendState = if (($statusOutput | Out-String) -match "Up") { "running" } else { "not running" }
- }
- }
- catch {}
-
Write-Host ""
Write-Host "===== Summary =====" -ForegroundColor Cyan
Write-Host "Action: Deployment DB restore executed ($operationState)"
- Write-Host "Updated: Database restored and matching uploads restored when available; services rebuilt"
- Write-Host "Live backend: $liveBackendState"
+ Write-Host "Updated: Database restored and matching uploads restored when available"
+ Write-Host "Database restore target: DATABASE_URL"
if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") {
Read-Host "Press Enter to close"
diff --git a/backend/scripts/setup-daily-backup-task.ps1 b/Old Scripts/backend/scripts/setup-daily-backup-task.ps1
similarity index 85%
rename from backend/scripts/setup-daily-backup-task.ps1
rename to Old Scripts/backend/scripts/setup-daily-backup-task.ps1
index 4dfc746..63fb3ac 100644
--- a/backend/scripts/setup-daily-backup-task.ps1
+++ b/Old Scripts/backend/scripts/setup-daily-backup-task.ps1
@@ -38,20 +38,12 @@ Write-Host "Verify with: schtasks /Query /TN $TaskName /V /FO LIST" -ForegroundC
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$root = Resolve-Path (Join-Path $scriptDir "..")
-$liveBackendState = "unavailable"
-try {
- $statusOutput = docker compose -f (Join-Path $root "docker-compose.yml") ps backend 2>$null
- if ($LASTEXITCODE -eq 0) {
- $liveBackendState = if (($statusOutput | Out-String) -match "Up") { "running" } else { "not running" }
- }
-}
-catch {}
Write-Host ""
Write-Host "===== Summary =====" -ForegroundColor Cyan
Write-Host "Action: Daily backup task configured"
Write-Host "Updated: Scheduled task '$TaskName' set to run at $RunAt"
-Write-Host "Live backend: $liveBackendState"
+Write-Host "Execution target: backup-deployment-db.ps1 (DATABASE_URL)"
if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") {
Read-Host "Press Enter to close"
diff --git a/Old Scripts/backend/scripts/update-live.ps1 b/Old Scripts/backend/scripts/update-live.ps1
new file mode 100644
index 0000000..41ccff4
--- /dev/null
+++ b/Old Scripts/backend/scripts/update-live.ps1
@@ -0,0 +1,24 @@
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$ServerHost,
+ [string]$User = "ec2-user",
+ [string]$ProjectPath = "/opt/devbits/backend",
+ [switch]$NoPause
+)
+
+$ErrorActionPreference = "Stop"
+
+Write-Host "Deploying backend on AWS host $User@$ServerHost using native Linux deploy script..." -ForegroundColor Yellow
+
+$remoteCommand = "cd $ProjectPath && ./scripts/deploy-aws-native.sh"
+ssh "$User@$ServerHost" $remoteCommand
+
+if ($LASTEXITCODE -ne 0) {
+ throw "Remote deploy failed with exit code $LASTEXITCODE"
+}
+
+Write-Host "Remote deploy completed successfully." -ForegroundColor Green
+
+if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") {
+ Read-Host "Press Enter to close"
+}
diff --git a/frontend/scripts/imagescale.ps1 b/Old Scripts/frontend/scripts/imagescale.ps1
similarity index 100%
rename from frontend/scripts/imagescale.ps1
rename to Old Scripts/frontend/scripts/imagescale.ps1
diff --git a/frontend/scripts/install-adb.ps1 b/Old Scripts/frontend/scripts/install-adb.ps1
similarity index 100%
rename from frontend/scripts/install-adb.ps1
rename to Old Scripts/frontend/scripts/install-adb.ps1
diff --git a/frontend/scripts/screenrecord.ps1 b/Old Scripts/frontend/scripts/screenrecord.ps1
similarity index 100%
rename from frontend/scripts/screenrecord.ps1
rename to Old Scripts/frontend/scripts/screenrecord.ps1
diff --git a/Powershell-Scripts/run-db-tests.ps1 b/Old Scripts/run-db-tests.ps1
similarity index 100%
rename from Powershell-Scripts/run-db-tests.ps1
rename to Old Scripts/run-db-tests.ps1
diff --git a/Powershell-Scripts/run-dev.ps1 b/Old Scripts/run-dev.ps1
similarity index 100%
rename from Powershell-Scripts/run-dev.ps1
rename to Old Scripts/run-dev.ps1
diff --git a/Powershell-Scripts/run-front.ps1 b/Old Scripts/run-front.ps1
similarity index 91%
rename from Powershell-Scripts/run-front.ps1
rename to Old Scripts/run-front.ps1
index 51af854..dee6dd6 100644
--- a/Powershell-Scripts/run-front.ps1
+++ b/Old Scripts/run-front.ps1
@@ -56,7 +56,7 @@ elseif ($Production) {
$mode = "production"
}
else {
- Write-Host "Select backend: 1) Production (devbits.ddns.net) 2) Local (LAN IP:8080)"
+ Write-Host "Select backend: 1) Production (devbits.app) 2) Local (LAN IP:8080)"
$selection = Read-Host "Choose [1/2]"
switch ($selection) {
"1" { $mode = "production" }
@@ -76,8 +76,8 @@ else {
Remove-Item Env:REACT_NATIVE_PACKAGER_HOSTNAME -ErrorAction SilentlyContinue
Remove-Item Env:EXPO_PACKAGER_HOSTNAME -ErrorAction SilentlyContinue
}
-$env:EXPO_PUBLIC_API_URL = "https://devbits.ddns.net"
-$env:EXPO_PUBLIC_API_FALLBACK_URL = "https://devbits.ddns.net"
+$env:EXPO_PUBLIC_API_URL = "https://devbits.app"
+$env:EXPO_PUBLIC_API_FALLBACK_URL = "https://devbits.app"
if ($mode -eq "local") {
$port = if ($env:EXPO_PUBLIC_LOCAL_API_PORT) { $env:EXPO_PUBLIC_LOCAL_API_PORT } else { "8080" }
@@ -88,7 +88,7 @@ if ($mode -eq "local") {
else {
$env:EXPO_PUBLIC_USE_LOCAL_API = "0"
Remove-Item Env:EXPO_PUBLIC_LOCAL_API_URL -ErrorAction SilentlyContinue
- Write-Host "Using production backend: https://devbits.ddns.net" -ForegroundColor Green
+ Write-Host "Using production backend: https://devbits.app" -ForegroundColor Green
}
Push-Location $frontendDir
diff --git a/Powershell-Scripts/run-tests.ps1 b/Old Scripts/run-tests.ps1
similarity index 100%
rename from Powershell-Scripts/run-tests.ps1
rename to Old Scripts/run-tests.ps1
diff --git a/README.md b/README.md
index 44d32c8..d13cbfe 100644
--- a/README.md
+++ b/README.md
@@ -33,12 +33,18 @@ Start only the frontend (choose production or local backend at launch):
./run-front.sh
```
-Start full local stack (dev PostgreSQL + backend + frontend in local API mode):
+Start local backend stack only (dev PostgreSQL + backend):
```bash
./run-dev.sh
```
+Then start frontend and choose local or live backend:
+
+```bash
+./run-front.sh
+```
+
Run backend tests using dockerized Go against the dev DB stack:
```bash
@@ -58,7 +64,8 @@ Scan the QR code with Expo Go on your phone. The app will automatically connect
### Verification Checklist
- Fresh clone frontend check: run `chmod +x run-front.sh run-dev.sh run-db-tests.sh`, then `./run-front.sh`, choose `Production`, and confirm Expo starts.
-- Full local stack: run `./run-dev.sh`, confirm backend health at `http://:8080/health`, then validate app API calls from a phone on same WiFi.
+- Backend stack: run `./run-dev.sh`, confirm backend health at `http://:8080/health`.
+- Frontend to local backend: run `./run-front.sh --local`, then validate app API calls from a phone on same WiFi.
- DB tests: run `./run-db-tests.sh` and confirm it exits with code `0`.
### Prerequisites
@@ -74,8 +81,18 @@ Scan the QR code with Expo Go on your phone. The app will automatically connect
For detailed instructions, see [INSTRUCTIONS.md](INSTRUCTIONS.md).
+## Static file sync
+
+`backend/api/static/` is the source of truth for compliance and deep-linking files
+(`apple-app-site-association`, `privacy-policy.html`, `account-deletion.html`).
+The same files are mirrored into `frontend/public/` for the Expo web build.
+
+Run `Bash-Scripts/sync-static.sh` after editing any file in `backend/api/static/`
+to keep the frontend copy in sync.
+
## Deployment DB scripts
All deployment database script commands and usage are documented in:
- [backend/scripts/README.md](backend/scripts/README.md)
+- [backend/docs/AWS_TRANSFER_NO_NGINX.md](backend/docs/AWS_TRANSFER_NO_NGINX.md)
diff --git a/backend/.env.example b/backend/.env.example
new file mode 100644
index 0000000..b7acfbc
--- /dev/null
+++ b/backend/.env.example
@@ -0,0 +1,25 @@
+# Copy this file to backend/.env and set strong values before deployment.
+# Linux/macOS: cp .env.example .env
+
+# Admin/API secrets (replace with strong random values)
+DEVBITS_ADMIN_KEY=replace-with-strong-admin-key
+DEVBITS_JWT_SECRET=replace-with-64-plus-char-random-secret
+
+# Preferred for AWS (RDS/Postgres)
+# Replace user/password/endpoint/db name with your real RDS values.
+# URL-encode special characters in password (for example ! -> %21).
+DATABASE_URL=postgres://postgres:replace-with-strong-password@your-rds-endpoint:5432/postgres?sslmode=require
+
+# Optional local fallback values (used only when DATABASE_URL is unset)
+POSTGRES_DB=devbits
+POSTGRES_USER=devbits
+POSTGRES_PASSWORD=replace-with-strong-password
+
+# Optional: comma-separated CORS origins for browser clients.
+DEVBITS_CORS_ORIGINS=https://devbits.app,https://www.devbits.app
+
+# AWS/ALB runtime settings
+DEVBITS_API_ADDR=0.0.0.0:8080
+DEVBITS_ADMIN_LOCAL_ONLY=0
+# Set true only when running behind a trusted proxy (ALB/nginx) that sets X-Forwarded-Proto.
+DEVBITS_TRUST_PROXY=true
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 363b076..c06c778 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -31,6 +31,7 @@ COPY --from=builder /app/main .
# Copy the database file
COPY --from=builder /app/api/internal/database/dev.sqlite3 ./api/internal/database/
COPY --from=builder /app/api/internal/database/create_tables.sql ./api/internal/database/
+COPY --from=builder /app/api/static ./api/static
# Copy admin UI static files
COPY --from=builder /app/api/admin ./admin
diff --git a/backend/api/internal/database/dev.sqlite3-shm b/backend/api/internal/database/dev.sqlite3-shm
index f70b4c0..0419e7c 100644
Binary files a/backend/api/internal/database/dev.sqlite3-shm and b/backend/api/internal/database/dev.sqlite3-shm differ
diff --git a/backend/api/internal/database/dev.sqlite3-wal b/backend/api/internal/database/dev.sqlite3-wal
index 3ea7a72..988ba46 100644
Binary files a/backend/api/internal/database/dev.sqlite3-wal and b/backend/api/internal/database/dev.sqlite3-wal differ
diff --git a/backend/api/internal/handlers/media_ingest.go b/backend/api/internal/handlers/media_ingest.go
index 2afdcfb..c9bcff8 100644
--- a/backend/api/internal/handlers/media_ingest.go
+++ b/backend/api/internal/handlers/media_ingest.go
@@ -86,7 +86,7 @@ func materializeMediaReference(raw string) (string, error) {
if parsed.Scheme == "http" || parsed.Scheme == "https" {
// Before downloading, check if the URL points to our own managed
- // uploads directory (e.g. "https://devbits.ddns.net/uploads/abc.jpg").
+ // uploads directory (e.g. "https://devbits.app/uploads/abc.jpg").
// If so, treat it as a local file to avoid a self-referential HTTP
// request that can hang or loop.
if filename, managed := extractManagedUploadFilename(parsed.Path); managed {
diff --git a/backend/api/internal/handlers/media_routes.go b/backend/api/internal/handlers/media_routes.go
index 3cac253..8589398 100644
--- a/backend/api/internal/handlers/media_routes.go
+++ b/backend/api/internal/handlers/media_routes.go
@@ -20,6 +20,11 @@ import (
const uploadDir = "uploads"
+// trustProxy is evaluated once at startup to avoid repeated os.Getenv calls
+// on every upload request. Set DEVBITS_TRUST_PROXY=true only when the backend
+// runs behind a trusted reverse proxy (e.g. AWS ALB) that sets X-Forwarded-Proto.
+var trustProxy = os.Getenv("DEVBITS_TRUST_PROXY") == "true"
+
var allowedImageExtensions = map[string]struct{}{
".jpg": {},
".jpeg": {},
@@ -209,17 +214,24 @@ func UploadMedia(context *gin.Context) {
scheme := "http"
if context.Request.TLS != nil {
scheme = "https"
+ } else if trustProxy {
+ if forwardedProto := strings.TrimSpace(context.GetHeader("X-Forwarded-Proto")); forwardedProto != "" {
+ scheme = strings.ToLower(strings.TrimSpace(strings.Split(forwardedProto, ",")[0]))
+ if scheme != "http" && scheme != "https" {
+ scheme = "http"
+ }
+ }
}
relativeURL := fmt.Sprintf("/%s/%s", uploadDir, filename)
absoluteURL := fmt.Sprintf("%s://%s%s", scheme, context.Request.Host, relativeURL)
context.JSON(http.StatusOK, gin.H{
- "url": relativeURL,
+ "url": relativeURL,
"absolute_url": absoluteURL,
- "filename": filename,
- "contentType": file.Header.Get("Content-Type"),
- "mediaType": mediaKind,
- "size": file.Size,
+ "filename": filename,
+ "contentType": file.Header.Get("Content-Type"),
+ "mediaType": mediaKind,
+ "size": file.Size,
})
}
diff --git a/backend/api/main.go b/backend/api/main.go
index f79184a..a90cf78 100644
--- a/backend/api/main.go
+++ b/backend/api/main.go
@@ -42,7 +42,8 @@ func getAllowedOrigins() []string {
}
return []string{
- "https://devbits.ddns.net",
+ "https://devbits.app",
+ "https://www.devbits.app",
"http://localhost:8081",
"http://localhost:19006",
"http://127.0.0.1:8081",
@@ -65,6 +66,7 @@ func HealthCheck(context *gin.Context) {
func resolveAdminDir() string {
candidates := []string{
"./admin",
+ "./api/admin",
"../admin",
"../../backend/api/admin",
"./backend/api/admin",
@@ -193,6 +195,20 @@ func main() {
} else {
log.Printf("INFO: admin UI available at /admin (key-protected)")
}
+ router.GET("/apple-app-site-association", func(c *gin.Context) {
+ c.Header("Content-Type", "application/json")
+ c.File("./api/static/apple-app-site-association")
+ })
+ router.GET("/.well-known/apple-app-site-association", func(c *gin.Context) {
+ c.Header("Content-Type", "application/json")
+ c.File("./api/static/apple-app-site-association")
+ })
+ router.GET("/.well-known/assetlinks.json", func(c *gin.Context) {
+ c.Header("Content-Type", "application/json")
+ c.File("./api/static/assetlinks.json")
+ })
+ router.StaticFile("/privacy-policy", "./api/static/privacy-policy.html")
+ router.StaticFile("/account-deletion", "./api/static/account-deletion.html")
router.GET("/", func(c *gin.Context) {
c.String(200, "Welcome to the DevBits API! Everything is running correctly.")
diff --git a/backend/api/static/account-deletion.html b/backend/api/static/account-deletion.html
new file mode 100644
index 0000000..8828e83
--- /dev/null
+++ b/backend/api/static/account-deletion.html
@@ -0,0 +1,333 @@
+
+
+
+
+
+
+
+ Become a Tester
+
+
+
+
+
+
+
+
+
+
Request account and data deletion — DevBits
+
+ This page explains how you can request deletion of your DevBits
+ account and associated data. It is provided by the developer shown on
+ the Google Play store listing: DevBits.
+
+
+
How to request deletion
+
+
+ Email us at
+ mail@elifouts.net with the
+ subject "Account Deletion Request" and include the username and the
+ email address associated with your account.
+
+
+ We will reply within 5 business days to confirm the request and may
+ ask for a brief confirmation to verify account ownership.
+
+
+ After confirmation we will process account deletion; you will
+ receive a confirmation email when deletion is completed.
+
+
+
+
What data we will delete
+
+
Account record (username, profile fields, avatar).
+
+ User-generated content you created: posts, comments, project pages
+ (these will be permanently removed).
+
+
Authentication credentials (passwords) and associated tokens.
+
Push notification token(s) associated with your account.
+
+
+
What data may be retained
+
+
+ Aggregated, anonymized usage analytics (non-identifying) for product
+ and security monitoring.
+
+
+ Server logs and backups (retained for security, fraud prevention,
+ and legal compliance). We purge backups according to the schedule
+ below.
+
+
+
+
Retention period
+
+ After you request deletion, we will remove your account and most
+ associated personal data within 30 days. Backups and
+ logs that cannot be instantly purged for technical or legal reasons
+ will be deleted or anonymized within 90 days unless
+ otherwise required by law.
+
+
+
Additional notes
+
+
+ If your content was shared by other users (e.g. forwarded messages),
+ copies may remain in their accounts until those users delete them.
+
+
+ Deleting your account is irreversible. If you wish to stop using
+ DevBits temporarily, you can request account deactivation instead —
+ contact us and we can advise.
+
+
+
+
+ If you need help with deletion or have legal requests, contact us at
+ mail@elifouts.com.
+
+ We, at Devbits ("we", "us", "our"), are committed to protecting your
+ privacy and to handling personal information in a responsible manner.
+ This policy describes how we collect, use, disclose, and protect
+ personal information through our website
+ devbits.app, our mobile
+ applications (Devbits on Android and iOS), and related services.
+
+ This policy applies to all visitors and users of our website and
+ services, including registered users and customers. It covers the
+ personal data we collect directly from you as well as data we collect
+ automatically.
+
+
+
Why we process personal information
+
+
+ Enhance user experience by understanding needs and preferences.
+
+
+ Provide timely support and respond to inquiries or service requests.
+
+
Operate and improve our products and services.
+
+ Perform business operations such as billing and account management.
+
+
+
+
Data we collect
+
+ We collect only the personal information necessary to provide and
+ improve our services. Examples include:
+
+
+
+ Identity & contact: first and last name, email address,
+ username.
+
+
+ Device & technical: operating system and version, device model,
+ app version, unique identifiers (where permitted), IP address.
+
+
+ Content: posts, comments, images and other content you upload or
+ create.
+
+
+ Authentication: credentials and tokens used to authenticate your
+ account.
+
+
Notifications: push tokens if you enable notifications.
+
+
+
How we use personal information
+
+
To provide, maintain and improve our services and features.
+
To authenticate users, prevent fraud, and secure accounts.
+
To communicate with users and respond to support requests.
+
For analytics, product improvement and crash reporting.
+
+ To comply with legal obligations and protect rights and safety.
+
+
+
+
Data storage & protection
+
+ Location: Personal information is stored on secure
+ servers in the United States. When transfers outside your jurisdiction
+ are necessary, we will take steps to ensure appropriate protections
+ are in place.
+
+
+ Security measures: We use encryption for data in
+ transit and at rest, access controls, and industry-standard security
+ practices to protect data. No system is completely secure; if a breach
+ occurs we will act in accordance with applicable laws.
+
+
+
Data Processing Agreements
+
+ We share data with third-party service providers (hosting, backups,
+ analytics, email delivery, payment processors) only as necessary.
+ Where required by law, we enter into Data Processing Agreements (DPAs)
+ that oblige providers to protect personal information and meet GDPR
+ standards.
+
+
+
Transparency & choices
+
+ We will notify you about material changes to our sharing practices and
+ obtain consent where required. You can manage preferences such as
+ notifications and certain profile fields within the App.
+
+
+
User rights (GDPR & general)
+
You may exercise the following rights where applicable:
+
+
Access: request a copy of personal data we hold about you.
+
Rectification: correct inaccurate or incomplete data.
+
+ Erasure: request deletion of personal data ('right to be
+ forgotten').
+
+
Restriction: request limitation of processing.
+
Portability: receive your data in a machine-readable format.
+
Object: object to processing, including for direct marketing.
+
+ Withdraw consent: withdraw consent where processing is based on it.
+
+
+
+ To exercise these rights contact us at mail@elifouts.net or +1-US
+ 5139078569. We may require identity verification to process requests.
+
+
+
Cookies & tracking technologies
+
+ We use cookies and similar technologies for essential functionality,
+ analytics, and optional advertising. A cookie banner on first visit
+ allows you to accept all, reject non-essential cookies, or customize
+ preferences. We use industry-standard providers and do not use cookies
+ to knowingly collect information from children under 13.
+
+
+
Children's privacy
+
+ Our services are not directed at children under 13. We do not
+ knowingly collect personal information from children under 13 without
+ verifiable parental consent. If we become aware we have collected such
+ data, we will delete it promptly. Parents may contact us to request
+ deletion.
+
+
+
US privacy & CCPA/CPRA
+
+ If you are a California resident you have additional rights under the
+ CCPA/CPRA, including the right to know what personal information we
+ collect, request deletion, and opt-out of certain sharing. To exercise
+ these rights contact mail@elifouts.net or call +1-US 5139078569.
+
+
+
Children under 13 & verifiable parental consent
+
+ If a feature requires parental consent, we will obtain verifiable
+ parental consent using reasonable methods (e.g. signed form or other
+ verification). Parents may exercise rights on behalf of their children
+ by contacting us.
+
+
+
Retention
+
+ We retain account and content data while your account exists. After
+ deletion requests we remove data from public view promptly and delete
+ backups within 90 days unless retention is required for legal reasons.
+
+
+
How to request deletion
+
+ Use our Delete Account page:
+ /account-deletion or email
+ mail@elifouts.net with subject "Account Deletion Request". We will
+ respond and verify ownership before completing deletion.
+
+
+
Changes to this policy
+
+ We may update this policy. We will publish the effective date above
+ and notify users for material changes.
+
+
+
App Store / Play Store data collection summary
+
+ For store listings and App Privacy forms, we collect the following
+ categories of data:
+
+
+
+ Identifiers: Email, username, device identifiers,
+ and account IDs — linked to user account.
+
+
+ Contact Info: Email address — linked to user
+ account for support and account recovery.
+
+
+ User Content: Posts, comments, images and uploaded
+ media — linked to user account.
+
+
+ Usage Data: App usage, analytics, crash reports,
+ device model, OS version — may be not linked for analytics,
+ but linked for debugging when necessary.
+
+
+ Diagnostics: Crash logs and server logs — may
+ contain identifiers and be linked to accounts for support.
+
+
+ Authentication Data: JWT auth tokens (stored
+ server-side and issued to clients) — used to authenticate requests;
+ tokens expire automatically.
+
+
+ Push Tokens: APNs/FCM push tokens — stored to
+ deliver notifications; these are linked to user accounts.
+
+
+
+
+ You may see these categories summarized in App Store Connect when
+ answering the App Privacy questions. If you need a one-line export for
+ the form:
+ Identifiers, Contact Info, User Content, Usage Data, Diagnostics —
+ all collected; Identifiers/Contact/User Content are linked to the
+ user; Usage/Diagnostics are sometimes linked for debugging.
+
+
+
Deep links & app links (Universal Links / App Links)
+
+ DevBits supports Universal Links (iOS) and App Links (Android). To
+ enable this we host two files on the server:
+
+
+
+ Apple (AASA):
+ /apple-app-site-association or
+ /.well-known/apple-app-site-association served as JSON
+ with application/json content-type. It lists the app's
+ App ID and allowed paths. Example template is available in the repo
+ and must include your Apple Team ID.
+
+
+ Android (assetlinks.json):
+ /.well-known/assetlinks.json served as JSON; it must
+ include your Android package and SHA256 fingerprint from your upload
+ keystore.
+
+
+
+
+ Hosting these files and adding the matching entitlements/associated
+ domains in the Apple Developer portal and Android intent filters is
+ required only if you want links to open directly in the native app. If
+ you do not require Universal Links, this step is optional but
+ recommended for a polished experience.
+
+
+
Contact
+
+ Questions or complaints: mail@elifouts.net | +1-US 5139078569. You may
+ also lodge a complaint with a supervisory authority if you believe we
+ have not complied with applicable law.
+
+
+
+ Last updated: 18/02/2026, 05:58:42 — This policy is provided for
+ informational purposes and does not constitute legal advice. If you
+ operate in regulated jurisdictions or collect data from children,
+ consider legal review.
+
+
+
+
diff --git a/backend/bin/api b/backend/bin/api
new file mode 100644
index 0000000..b94c784
Binary files /dev/null and b/backend/bin/api differ
diff --git a/backend/bin/devbits-api b/backend/bin/devbits-api
new file mode 100644
index 0000000..5a15837
Binary files /dev/null and b/backend/bin/devbits-api differ
diff --git a/backend/deploy/systemd/devbits-api.service b/backend/deploy/systemd/devbits-api.service
new file mode 100644
index 0000000..17a18b0
--- /dev/null
+++ b/backend/deploy/systemd/devbits-api.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=DevBits API Service
+After=network.target
+
+[Service]
+Type=simple
+User=__SERVICE_USER__
+Group=__SERVICE_USER__
+WorkingDirectory=__WORKDIR__
+EnvironmentFile=__ENV_FILE__
+ExecStart=__BINARY_PATH__
+Restart=always
+RestartSec=3
+LimitNOFILE=65535
+
+[Install]
+WantedBy=multi-user.target
diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml
index 349c2f0..dd96c6f 100644
--- a/backend/docker-compose.yml
+++ b/backend/docker-compose.yml
@@ -16,8 +16,6 @@ services:
interval: 5s
timeout: 5s
retries: 20
- networks:
- - app-network
backend:
build:
@@ -32,8 +30,6 @@ services:
- "8080:8080"
security_opt:
- no-new-privileges:true
- networks:
- - app-network
depends_on:
db:
condition: service_healthy
@@ -41,31 +37,7 @@ services:
- DATABASE_URL=postgres://${POSTGRES_USER:-devbits}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@db:5432/${POSTGRES_DB:-devbits}?sslmode=disable
- DEVBITS_ADMIN_KEY=${DEVBITS_ADMIN_KEY}
- DEVBITS_ADMIN_LOCAL_ONLY=${DEVBITS_ADMIN_LOCAL_ONLY:-0}
-
- nginx:
- image: nginx:latest
- container_name: devbits-nginx
- restart: unless-stopped
- ports:
- - "80:80"
- - "443:443"
- volumes:
- - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- # Mount volumes for Let's Encrypt certificates
- - ./nginx/certs:/etc/letsencrypt
- # Serve static site assets (public pages + screenshots)
- - ../frontend/public:/usr/share/nginx/html:ro
- depends_on:
- backend:
- condition: service_started
- security_opt:
- - no-new-privileges:true
- networks:
- - app-network
-
-networks:
- app-network:
- driver: bridge
+ - DEVBITS_CORS_ORIGINS=${DEVBITS_CORS_ORIGINS:-https://devbits.app,https://www.devbits.app}
volumes:
postgres-data:
diff --git a/backend/docs/AWS_TRANSFER_NO_NGINX.md b/backend/docs/AWS_TRANSFER_NO_NGINX.md
new file mode 100644
index 0000000..db3a01f
--- /dev/null
+++ b/backend/docs/AWS_TRANSFER_NO_NGINX.md
@@ -0,0 +1,90 @@
+# DevBits AWS Transfer (Native Backend, No Docker, No Nginx)
+
+This runbook deploys the Go backend as a native Linux service on EC2.
+
+## Target architecture
+
+- Route 53 (`devbits.app`, optional `www.devbits.app`)
+- ACM certificate
+- Application Load Balancer (HTTPS 443)
+- EC2 instance running `devbits-api` via `systemd`
+- PostgreSQL on AWS RDS (recommended) or another managed Postgres
+
+## What changed in this branch
+
+- `devbits.ddns.net` defaults moved to `devbits.app`.
+- nginx runtime dependency removed from deployment path.
+- Backend serves:
+ - `/apple-app-site-association`
+ - `/.well-known/assetlinks.json`
+ - `/privacy-policy`
+ - `/account-deletion`
+- Upload absolute URLs now honor `X-Forwarded-Proto` (correct behind ALB).
+- Added native deploy scripts:
+ - `backend/scripts/build-backend-linux.sh`
+ - `backend/scripts/install-aws-systemd-service.sh`
+ - `backend/scripts/deploy-aws-native.sh`
+ - `backend/scripts/update-live.sh` (wrapper)
+
+## What you give the AWS account owner
+
+1. Repo URL + branch name.
+2. Domain: `devbits.app` (+ optional `www.devbits.app`).
+3. Region (example: `us-east-1`).
+4. These env values for `backend/.env`:
+ - `DATABASE_URL=postgres://...` (RDS endpoint, db, user, password, sslmode=require)
+ - `DEVBITS_JWT_SECRET`
+ - `DEVBITS_ADMIN_KEY`
+ - `DEVBITS_ADMIN_LOCAL_ONLY=0` (or `1` for localhost-only admin)
+ - `DEVBITS_CORS_ORIGINS=https://devbits.app,https://www.devbits.app`
+ - `DEVBITS_API_ADDR=0.0.0.0:8080`
+5. Optional data migration files:
+ - DB dump (`devbits-db-*.sql`)
+ - uploads archive
+
+## AWS setup steps (admin)
+
+1. Create Route 53 records for `devbits.app` and `www.devbits.app`.
+2. Request ACM cert for both names.
+3. Create ALB:
+ - Listener `80` -> redirect to `443`
+ - Listener `443` -> target group on EC2 `:8080`
+ - Health check path `/health`
+4. Create EC2 (Amazon Linux 2023 recommended).
+5. Security groups:
+ - ALB SG: inbound `80/443` from internet
+ - EC2 SG: inbound `8080` from ALB SG only, `22` from admin IP only
+6. Provision EC2:
+ - Install `git` and `tar`: `sudo dnf install -y git tar`
+ - Install Go `1.24.x` from `go.dev` tarball to `/usr/local/go`
+ - Clone repo to `/opt/devbits` (typically as `ec2-user`)
+ - `cd /opt/devbits/backend`
+ - `cp .env.example .env` and fill real values
+ - `./scripts/deploy-aws-native.sh`
+7. Verify:
+ - `https://devbits.app/health`
+ - `https://devbits.app/privacy-policy`
+ - `https://devbits.app/account-deletion`
+ - `https://devbits.app/apple-app-site-association`
+ - `https://devbits.app/.well-known/assetlinks.json`
+
+## Updating backend after code changes
+
+On EC2:
+
+```bash
+cd /opt/devbits
+git pull origin
+cd backend
+./scripts/update-live.sh
+```
+
+Amazon Linux notes:
+
+- Default SSH user is usually `ec2-user`.
+- Use `dnf` (not `apt`) for packages.
+- Keep `/opt/devbits` owned by the deploy user so build/deploy scripts can run without permission issues.
+
+## Notes
+
+- Docker is still available in this repo for local workflows, but AWS deployment in this runbook is native (`systemd`) and does not require Docker or nginx.
diff --git a/backend/go.mod b/backend/go.mod
index af52653..ff6845e 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -7,6 +7,7 @@ require (
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/gorilla/websocket v1.5.3
+ github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.11.2
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
@@ -28,7 +29,6 @@ require (
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.6.0 // indirect
- github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/text v0.2.0 // indirect
diff --git a/backend/nginx/nginx.conf b/backend/nginx/nginx.conf
deleted file mode 100644
index 28f1161..0000000
--- a/backend/nginx/nginx.conf
+++ /dev/null
@@ -1,579 +0,0 @@
-worker_processes 1;
-
-events {
- worker_connections 1024;
-}
-
-http {
- upstream backend_upstream {
- server backend:8080;
- keepalive 64;
- }
-
- map $http_upgrade $connection_upgrade {
- default upgrade;
- '' close;
- }
-
- map "$request_method $request_uri" $loggable {
- default 1;
- "GET /favicon.ico" 0;
- "GET /apple-touch-icon.png" 0;
- "GET /apple-touch-icon-precomposed.png" 0;
- "POST /" 0;
- }
-
- map $request_uri $honeypot_hit {
- default 0;
- ~*^/(wp-admin|wp-login\.php|xmlrpc\.php|phpmyadmin|pma|\.git|\.env|cgi-bin|boaform|actuator|manager/html|server-status|vendor/phpunit|\.well-known/pki-validation) 1;
- }
-
- map $honeypot_hit $honeypot_loggable {
- default 0;
- 1 1;
- }
-
- access_log /dev/stdout combined if=$loggable;
- access_log /dev/stdout combined if=$honeypot_loggable;
- error_log /dev/stderr warn;
-
- limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=10r/m;
- limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/s;
-
- sendfile on;
- tcp_nopush on;
- tcp_nodelay on;
- keepalive_timeout 30;
- keepalive_requests 1000;
- types_hash_max_size 2048;
-
- gzip on;
- gzip_vary on;
- gzip_min_length 1024;
- gzip_comp_level 6;
- gzip_types
- text/plain
- text/css
- text/xml
- application/xml
- application/json
- application/javascript
- image/svg+xml;
-
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
- client_max_body_size 64m;
- proxy_set_header Authorization $http_authorization;
- server_tokens off;
-
- # HTTP server for local development and health checks
- server {
- listen 80 default_server;
- server_name _;
- add_header X-Content-Type-Options "nosniff" always;
- add_header X-Frame-Options "DENY" always;
- add_header Referrer-Policy "same-origin" always;
-
- location ~* ^/(wp-admin|wp-login\.php|xmlrpc\.php|phpmyadmin|pma|\.git|\.env|cgi-bin|boaform|actuator|manager/html|server-status|vendor/phpunit|\.well-known/pki-validation) {
- return 444;
- }
-
- location ~* ^/(media/upload|users/.+/(profile-picture|update))$ {
- client_max_body_size 64m;
- client_body_timeout 30s;
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Connection "";
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- proxy_request_buffering on;
- proxy_buffering on;
- proxy_read_timeout 30s;
- proxy_send_timeout 30s;
- add_header Cache-Control "no-store" always;
- }
-
- location ~* ^/users/[^/]+$ {
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Connection "";
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- proxy_request_buffering on;
- proxy_buffering on;
- proxy_read_timeout 30s;
- proxy_send_timeout 30s;
- add_header Cache-Control "no-store" always;
- }
-
- location ~ ^/(auth|feed|users|projects|posts|comments|notifications|messages|media|health)(/|$) {
- limit_req zone=api_limit burst=120 nodelay;
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- location ^~ /uploads/ {
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Connection "";
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- add_header Cache-Control "no-cache, max-age=0, must-revalidate" always;
- }
-
- location ~* \.(png|jpg|jpeg|gif|webp|svg|ico)$ {
- root /usr/share/nginx/html;
- try_files $uri =404;
- add_header Cache-Control "public, max-age=86400" always;
- }
-
- location = /Cards.svg {
- default_type image/svg+xml;
- alias /usr/share/nginx/html/Cards.svg;
- add_header Cache-Control "public, max-age=86400" always;
- }
-
- location = /cards.svg {
- default_type image/svg+xml;
- alias /usr/share/nginx/html/Cards.svg;
- add_header Cache-Control "public, max-age=86400" always;
- }
-
- location / {
- root /usr/share/nginx/html;
- try_files $uri $uri/ @backend;
- }
-
- location @backend {
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- # Serve Apple App Site Association for Universal Links
- location = /apple-app-site-association {
- default_type application/json;
- alias /usr/share/nginx/html/apple-app-site-association;
- }
-
- # Serve Android assetlinks (App Links)
- location = /.well-known/assetlinks.json {
- default_type application/json;
- alias /usr/share/nginx/html/.well-known/assetlinks.json;
- }
-
- location /api/ {
- limit_req zone=api_limit burst=120 nodelay;
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- location = /admin {
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- location ^~ /admin/ {
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- location = /api/auth/login {
- limit_req zone=auth_limit burst=40 nodelay;
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- location = /api/auth/register {
- limit_req zone=auth_limit burst=40 nodelay;
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
- }
-
- # Redirect all HTTP traffic to HTTPS for production domain
- server {
- listen 80;
- server_name devbits.ddns.net; # <-- IMPORTANT: Replace with your DDNS domain
- return 301 https://$host$request_uri;
- }
-
- # HTTPS server
- server {
- listen 443 ssl http2;
- server_name devbits.ddns.net; # <-- IMPORTANT: Replace with your DDNS domain
- add_header X-Content-Type-Options "nosniff" always;
- add_header X-Frame-Options "DENY" always;
- add_header Referrer-Policy "same-origin" always;
- add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
-
- location ~* ^/(wp-admin|wp-login\.php|xmlrpc\.php|phpmyadmin|pma|\.git|\.env|cgi-bin|boaform|actuator|manager/html|server-status|vendor/phpunit|\.well-known/pki-validation) {
- return 444;
- }
-
- # SSL Certificate
- ssl_certificate /etc/letsencrypt/live/devbits.ddns.net/fullchain.pem; # <-- IMPORTANT: Replace with your domain
- ssl_certificate_key /etc/letsencrypt/live/devbits.ddns.net/privkey.pem; # <-- IMPORTANT: Replace with your domain
-
- # Improve SSL security
- ssl_protocols TLSv1.2 TLSv1.3;
- ssl_prefer_server_ciphers on;
- ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
-
- # Session / ticket settings
- ssl_session_cache shared:SSL:50m;
- ssl_session_timeout 1d;
- ssl_session_tickets on;
-
- # OCSP stapling disabled because current certificate chain has no responder URL.
- ssl_stapling off;
- ssl_stapling_verify off;
- resolver 1.1.1.1 8.8.8.8 8.8.4.4 ipv6=off valid=300s;
- resolver_timeout 5s;
- ssl_trusted_certificate /etc/letsencrypt/live/devbits.ddns.net/chain.pem;
-
- # Disable gzip on TLS endpoint to mitigate BREACH risk for sensitive responses
- gzip off;
-
- location ~* ^/(media/upload|users/.+/(profile-picture|update))$ {
- client_max_body_size 64m;
- client_body_timeout 180s;
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Connection "";
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- proxy_request_buffering on;
- proxy_buffering on;
- proxy_read_timeout 180s;
- proxy_send_timeout 180s;
- add_header Cache-Control "no-store" always;
- }
-
- location ~* ^/users/[^/]+$ {
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Connection "";
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- proxy_request_buffering on;
- proxy_buffering on;
- proxy_read_timeout 180s;
- proxy_send_timeout 180s;
- add_header Cache-Control "no-store" always;
- }
-
- location ~ ^/(auth|feed|users|projects|posts|comments|notifications|messages|media|health)(/|$) {
- limit_req zone=api_limit burst=120 nodelay;
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- location ^~ /uploads/ {
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Connection "";
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- add_header Cache-Control "no-cache, max-age=0, must-revalidate" always;
- }
-
- location ~* \.(png|jpg|jpeg|gif|webp|svg|ico)$ {
- root /usr/share/nginx/html;
- try_files $uri =404;
- add_header Cache-Control "public, max-age=86400" always;
- }
-
- location = /Cards.svg {
- default_type image/svg+xml;
- alias /usr/share/nginx/html/Cards.svg;
- add_header Cache-Control "public, max-age=86400" always;
- }
-
- location = /cards.svg {
- default_type image/svg+xml;
- alias /usr/share/nginx/html/Cards.svg;
- add_header Cache-Control "public, max-age=86400" always;
- }
-
- location / {
- root /usr/share/nginx/html;
- try_files $uri $uri/ @backend;
- }
-
- # Serve Apple App Site Association for Universal Links
- location = /apple-app-site-association {
- default_type application/json;
- alias /usr/share/nginx/html/apple-app-site-association;
- }
-
- # Serve Android assetlinks (App Links)
- location = /.well-known/assetlinks.json {
- default_type application/json;
- alias /usr/share/nginx/html/.well-known/assetlinks.json;
- }
-
- location @backend {
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- # Serve static pages directly from nginx
- location = /account-deletion {
- default_type text/html;
- alias /usr/share/nginx/html/account-deletion.html;
- }
-
- location = /account-deletion.html {
- default_type text/html;
- alias /usr/share/nginx/html/account-deletion.html;
- }
-
- # Serve the Privacy Policy page required by store listings
- location = /privacy-policy {
- default_type text/html;
- alias /usr/share/nginx/html/privacy-policy.html;
- }
-
- location = /privacy-policy.html {
- default_type text/html;
- alias /usr/share/nginx/html/privacy-policy.html;
- }
-
- location = /api/auth/login {
- limit_req zone=auth_limit burst=20 nodelay;
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- location = /api/auth/register {
- limit_req zone=auth_limit burst=20 nodelay;
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- location /api/ {
- limit_req zone=api_limit burst=120 nodelay;
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- location = /admin {
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- location ^~ /admin/ {
- proxy_pass http://backend_upstream;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection $connection_upgrade;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_cache off;
- proxy_cache_bypass 1;
- proxy_no_cache 1;
- add_header Cache-Control "no-store" always;
- }
-
- # Serve the CSAE (Child Sexual Abuse & Exploitation) standards page
- location = /csae-standards {
- default_type text/html;
- alias /usr/share/nginx/html/csae-standards.html;
- }
-
- location = /csae-standards.html {
- default_type text/html;
- alias /usr/share/nginx/html/csae-standards.html;
- }
-
- # Serve the tester application page
- location = /tester-application {
- default_type text/html;
- alias /usr/share/nginx/html/tester-application.html;
- }
-
- location = /tester-application.html {
- default_type text/html;
- alias /usr/share/nginx/html/tester-application.html;
- }
-
- # Serve features page
- location = /features {
- default_type text/html;
- alias /usr/share/nginx/html/features.html;
- }
-
- location = /features.html {
- default_type text/html;
- alias /usr/share/nginx/html/features.html;
- }
-
- # Serve about page
- location = /about {
- default_type text/html;
- alias /usr/share/nginx/html/about.html;
- }
-
- location = /about.html {
- default_type text/html;
- alias /usr/share/nginx/html/about.html;
- }
-
- }
-}
diff --git a/backend/scripts/README.md b/backend/scripts/README.md
index 829b448..7748d6c 100644
--- a/backend/scripts/README.md
+++ b/backend/scripts/README.md
@@ -1,165 +1,118 @@
-# DevBits Database Scripts
+# DevBits Backend Scripts
-All deployment database scripts are in this folder.
+Run scripts from `backend/`.
-## Environment separation (important)
+## 1) Update backend on AWS
-### Local DB (development machine)
-
-Run from project root:
-
-```powershell
-cd c:\Users\eligf\DevBits
-```
-
-Use compose file path explicitly:
-
-```powershell
-docker compose -f backend/docker-compose.yml up -d
-docker compose -f backend/docker-compose.yml logs -f db
-```
-
-### Live DB (deployed server)
-
-Run on server in backend directory:
+Build locally, then copy binary to EC2 (recommended):
```bash
-cd /path/to/DevBits/backend
-docker compose up -d
-docker compose logs -f db
+# local machine
+cd /home/ws-73/OldFiles/projects/DevBits/backend
+TARGET_GOOS=linux TARGET_GOARCH=amd64 ./scripts/build-backend-linux.sh
+scp -i ./bin/devbits-api ec2-user@:/tmp/devbits-api
```
-Only run reset/restore in the environment you mean to modify.
-
-## Script location
+On the EC2 host:
-Run script commands from `backend`:
-
-```powershell
+```bash
+cd /opt/devbits
+# Save any local EC2 edits before pulling
+git stash push -u -m "ec2-local-before-pull-$(date +%Y%m%d-%H%M%S)"
+git pull origin aws-ready-main
cd backend
+sudo mv /tmp/devbits-api ./bin/devbits-api
+sudo chown ec2-user:ec2-user ./bin/devbits-api
+sudo chmod +x ./bin/devbits-api
+sudo ./scripts/install-aws-systemd-service.sh
```
-## Required env file
+If you intentionally want to discard local EC2 changes instead:
-Before running deploy/reset/update scripts, ensure `backend/.env` exists:
-
-```powershell
-Copy-Item .env.example .env
+```bash
+cd /opt/devbits
+git reset --hard
+git clean -fd
+git pull origin aws-ready-main
+cd backend
+./scripts/deploy-aws-native.sh
```
-Set a strong `POSTGRES_PASSWORD` value in `.env`.
-
-## Scripts
-
-- `scripts/reset-deployment-db.ps1` / `scripts/reset-deployment-db.sh`
-- `scripts/backup-deployment-db.ps1` / `scripts/backup-deployment-db.sh`
-- `scripts/restore-deployment-db.ps1` / `scripts/restore-deployment-db.sh`
-- `scripts/setup-daily-backup-task.ps1`
-- `scripts/disable-daily-backup-task.ps1`
+Verify:
-## 1) Reset DB (blank slate)
-
-Warning: this wipes all app data in that environment.
-
-PowerShell:
-
-```powershell
-./scripts/reset-deployment-db.ps1
+```bash
+sudo systemctl status devbits-api --no-pager
+curl -i http://127.0.0.1:8080/health
```
-Keep uploads while resetting only DB volume:
-
-```powershell
-./scripts/reset-deployment-db.ps1 -KeepUploads
-```
+## 1.1) Extra AWS checks
-Bash:
+Run on EC2:
```bash
-./scripts/reset-deployment-db.sh
-./scripts/reset-deployment-db.sh --keep-uploads
-```
-
-## 2) Backup DB (single-backup retention)
+# Service state
+sudo systemctl is-active devbits-api
+sudo systemctl status devbits-api --no-pager
-Safe for both local and live. Run it in the target environment.
+# Process is listening on 8080
+sudo ss -ltnp | grep ':8080'
-PowerShell:
+# Recent service logs
+sudo journalctl -u devbits-api -n 150 --no-pager
-```powershell
-./scripts/backup-deployment-db.ps1
+# Follow logs live while testing app traffic
+sudo journalctl -u devbits-api -f
```
-Bash:
+Database connectivity check (from EC2):
```bash
-./scripts/backup-deployment-db.sh
-```
-
-Backup location:
-
-- `backend/backups/db`
-
-Retention policy:
-
-- keeps only the newest `devbits-*.sql`
-- deletes older backup files automatically
-
-Backup type:
-
-- Logical SQL dump created with `pg_dump` from the running DB container
-- Not a Docker volume snapshot/image snapshot
-
-## 3) Restore DB from latest backup
+# Ensure PostgreSQL client tools are installed
+sudo dnf install -y postgresql15
-Warning: restore terminates sessions and recreates DB in that environment.
-
-PowerShell:
-
-```powershell
-./scripts/restore-deployment-db.ps1
+# Uses DATABASE_URL from backend/.env
+cd /opt/devbits/backend
+set -a; . ./.env; set +a
+psql "$DATABASE_URL" -c "select current_user, current_database();"
```
-Bash:
+DNS/public checks (from local machine or EC2):
```bash
-./scripts/restore-deployment-db.sh
+dig +short devbits.app
+curl -i https://devbits.app/health
+curl -i https://devbits.app/privacy-policy
+curl -i https://devbits.app/.well-known/assetlinks.json
+curl -i https://devbits.app/apple-app-site-association
```
-Restore behavior:
-
-- picks latest backup file from `backend/backups/db`
-- terminates active DB sessions
-- drops and recreates `devbits`
-- applies SQL dump
-
-## 4) Enable daily auto backup (Windows)
-
-Create a scheduled task at 03:00 daily:
+Target group health check path should return 200:
-```powershell
-./scripts/setup-daily-backup-task.ps1
+```bash
+curl -i http://127.0.0.1:8080/health
```
-Custom time:
+## 2) Script usage
-```powershell
-./scripts/setup-daily-backup-task.ps1 -RunAt "01:30"
-```
+Deploy/build:
-Notes:
+- `scripts/build-backend-linux.sh`
+ - Build backend binary to `bin/devbits-api`.
+- `scripts/install-aws-systemd-service.sh`
+ - Install/restart `devbits-api` systemd service.
+- `scripts/deploy-aws-native.sh`
+ - Build + install/restart in one command.
+- `scripts/update-live.sh`
+ - Wrapper for `deploy-aws-native.sh`.
-- Script tries `SYSTEM` first.
-- If shell is not elevated, it falls back to current-user mode.
+Database scripts (use `DATABASE_URL` in `backend/.env`):
-Verify task:
+- `scripts/reset-deployment-db.sh`
+- `--keep-uploads` to keep uploads.
+- `scripts/backup-deployment-db.sh`
+- `scripts/restore-deployment-db.sh`
+- restores latest `devbits-db-*.sql` and matching uploads archive if present.
-```powershell
-schtasks /Query /TN DevBitsDailyDbBackup /V /FO LIST
-```
+Required tools for DB scripts:
-## 5) Disable daily auto backup (Windows)
-
-```powershell
-./scripts/disable-daily-backup-task.ps1
-```
+- Linux: `postgresql` client package (`psql`, `pg_dump`)
diff --git a/backend/scripts/backup-deployment-db.sh b/backend/scripts/backup-deployment-db.sh
index 2463bed..ce10b0d 100644
--- a/backend/scripts/backup-deployment-db.sh
+++ b/backend/scripts/backup-deployment-db.sh
@@ -1,28 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
-if [ "$EUID" -ne 0 ]; then
- echo "Please run as root"
- sudo "$0" "$@"
- exit
-fi
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$ROOT_DIR"
BACKUP_DIR="${1:-backups/db}"
echo "Creating deployment database backup..."
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
-cd "$ROOT_DIR"
+if [[ ! -f ".env" ]]; then
+ echo "Missing $ROOT_DIR/.env. Create it from backend/.env.example and set DATABASE_URL." >&2
+ exit 1
+fi
+
+set -a
+. ./.env
+set +a
-if [[ -f ".env" ]]; then
- set -a
- . ./.env
- set +a
+if [[ -z "${DATABASE_URL:-}" ]]; then
+ echo "Missing DATABASE_URL in $ROOT_DIR/.env" >&2
+ exit 1
fi
-DB_USER="${POSTGRES_USER:-devbits}"
-DB_NAME="${POSTGRES_DB:-devbits}"
+if ! command -v pg_dump >/dev/null 2>&1; then
+ echo "pg_dump is required. Install PostgreSQL client tools first." >&2
+ exit 1
+fi
mkdir -p "$BACKUP_DIR"
@@ -30,42 +34,34 @@ TIMESTAMP="$(date +"%Y%m%d-%H%M%S")"
DB_BACKUP_FILE_NAME="devbits-db-${TIMESTAMP}.sql"
DB_OUTPUT_FILE="${BACKUP_DIR}/${DB_BACKUP_FILE_NAME}"
-docker compose exec -T db pg_dump -U "$DB_USER" -d "$DB_NAME" --no-owner --no-privileges > "$DB_OUTPUT_FILE"
+pg_dump "$DATABASE_URL" --no-owner --no-privileges > "$DB_OUTPUT_FILE"
if [[ ! -s "$DB_OUTPUT_FILE" ]]; then
echo "Database backup file is empty or missing. Aborting." >&2
exit 1
fi
+
echo "Database backup created: $DB_OUTPUT_FILE"
UPLOADS_DIR="uploads"
UPLOADS_BACKUP_FILE_NAME="devbits-uploads-${TIMESTAMP}.tar.gz"
UPLOADS_OUTPUT_FILE="${BACKUP_DIR}/${UPLOADS_BACKUP_FILE_NAME}"
-if [ -d "$UPLOADS_DIR" ]; then
+if [[ -d "$UPLOADS_DIR" ]]; then
tar -czf "$UPLOADS_OUTPUT_FILE" -C "$UPLOADS_DIR" .
echo "Uploads backup created: $UPLOADS_OUTPUT_FILE"
else
echo "Uploads directory not found, skipping backup."
fi
-# Retention: keep only the latest db backup and the latest uploads backup
+# Retention: keep only the latest DB backup and latest uploads backup.
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'devbits-db-*.sql' | sort | head -n -1 | xargs -r rm
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'devbits-uploads-*.tar.gz' | sort | head -n -1 | xargs -r rm
echo "Retention applied: only latest backup of each type is kept."
-live_backend_state="unavailable"
-if backend_status_output="$(docker compose ps backend 2>/dev/null)"; then
- if echo "$backend_status_output" | grep -q "Up"; then
- live_backend_state="running"
- else
- live_backend_state="not running"
- fi
-fi
-
echo
echo "===== Summary ====="
echo "Action: Deployment backup created"
echo "Updated: Latest DB + uploads backup files retained"
-echo "Live backend: $live_backend_state"
+echo "Database backup target: DATABASE_URL"
diff --git a/backend/scripts/build-backend-linux.sh b/backend/scripts/build-backend-linux.sh
new file mode 100644
index 0000000..95883f8
--- /dev/null
+++ b/backend/scripts/build-backend-linux.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+cd "$ROOT_DIR"
+
+mkdir -p bin uploads
+
+TARGET_GOOS="${TARGET_GOOS:-linux}"
+if [[ -n "${TARGET_GOARCH:-}" ]]; then
+ TARGET_GOARCH="${TARGET_GOARCH}"
+else
+ case "$(uname -m)" in
+ x86_64) TARGET_GOARCH="amd64" ;;
+ aarch64|arm64) TARGET_GOARCH="arm64" ;;
+ *) TARGET_GOARCH="amd64" ;;
+ esac
+fi
+OUTPUT_PATH="${OUTPUT_PATH:-$ROOT_DIR/bin/devbits-api}"
+
+echo "Building DevBits backend for ${TARGET_GOOS}/${TARGET_GOARCH}..."
+CGO_ENABLED=0 GOOS="$TARGET_GOOS" GOARCH="$TARGET_GOARCH" go build -trimpath -ldflags="-s -w" -o "$OUTPUT_PATH" ./api
+
+echo "Build complete: $OUTPUT_PATH"
diff --git a/backend/scripts/deploy-aws-native.sh b/backend/scripts/deploy-aws-native.sh
new file mode 100644
index 0000000..a15eb8f
--- /dev/null
+++ b/backend/scripts/deploy-aws-native.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+cd "$ROOT_DIR"
+
+if [[ ! -f ".env" ]]; then
+ echo "Missing $ROOT_DIR/.env. Create it from backend/.env.example and set real values." >&2
+ exit 1
+fi
+
+echo "Building backend binary..."
+"$ROOT_DIR/scripts/build-backend-linux.sh"
+
+if [[ "$EUID" -eq 0 ]]; then
+ "$ROOT_DIR/scripts/install-aws-systemd-service.sh"
+else
+ sudo "$ROOT_DIR/scripts/install-aws-systemd-service.sh"
+fi
+
+echo
+echo "===== Summary ====="
+echo "Action: Native AWS deploy completed"
+echo "Binary: $ROOT_DIR/bin/devbits-api"
+echo "Service: ${DEVBITS_SERVICE_NAME:-devbits-api}"
diff --git a/backend/scripts/install-aws-systemd-service.sh b/backend/scripts/install-aws-systemd-service.sh
new file mode 100644
index 0000000..e666325
--- /dev/null
+++ b/backend/scripts/install-aws-systemd-service.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ "$EUID" -ne 0 ]]; then
+ echo "Please run as root"
+ sudo "$0" "$@"
+ exit
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+SERVICE_NAME="${DEVBITS_SERVICE_NAME:-devbits-api}"
+SERVICE_USER="${DEVBITS_SERVICE_USER:-${SUDO_USER:-ec2-user}}"
+WORK_DIR="${DEVBITS_WORKDIR:-$ROOT_DIR}"
+ENV_FILE="${DEVBITS_ENV_FILE:-$WORK_DIR/.env}"
+BINARY_PATH="${DEVBITS_BINARY_PATH:-$WORK_DIR/bin/devbits-api}"
+TEMPLATE_PATH="$ROOT_DIR/deploy/systemd/devbits-api.service"
+SERVICE_PATH="/etc/systemd/system/${SERVICE_NAME}.service"
+
+if [[ ! -f "$ENV_FILE" ]]; then
+ echo "Missing env file: $ENV_FILE" >&2
+ echo "Create it from backend/.env.example before installing service." >&2
+ exit 1
+fi
+
+if [[ ! -x "$BINARY_PATH" ]]; then
+ echo "Missing executable binary: $BINARY_PATH" >&2
+ echo "Run ./scripts/build-backend-linux.sh first." >&2
+ exit 1
+fi
+
+if [[ ! -f "$TEMPLATE_PATH" ]]; then
+ echo "Missing systemd service template: $TEMPLATE_PATH" >&2
+ echo "Ensure the deploy/systemd/devbits-api.service template is present." >&2
+ exit 1
+fi
+
+if ! id "$SERVICE_USER" >/dev/null 2>&1; then
+ echo "Service user does not exist: $SERVICE_USER" >&2
+ exit 1
+fi
+
+tmp_service="$(mktemp)"
+trap 'rm -f "$tmp_service"' EXIT
+
+sed \
+ -e "s|__SERVICE_USER__|$SERVICE_USER|g" \
+ -e "s|__WORKDIR__|$WORK_DIR|g" \
+ -e "s|__ENV_FILE__|$ENV_FILE|g" \
+ -e "s|__BINARY_PATH__|$BINARY_PATH|g" \
+ "$TEMPLATE_PATH" > "$tmp_service"
+
+install -m 0644 "$tmp_service" "$SERVICE_PATH"
+
+systemctl daemon-reload
+systemctl enable "$SERVICE_NAME"
+systemctl restart "$SERVICE_NAME"
+
+echo "Service installed: $SERVICE_PATH"
+systemctl --no-pager --full status "$SERVICE_NAME" | sed -n '1,25p'
diff --git a/backend/scripts/reset-deployment-db.sh b/backend/scripts/reset-deployment-db.sh
index 868dbfe..db7c2b8 100644
--- a/backend/scripts/reset-deployment-db.sh
+++ b/backend/scripts/reset-deployment-db.sh
@@ -3,12 +3,6 @@ set -euo pipefail
KEEP_UPLOADS="${1:-}"
-if [ "$EUID" -ne 0 ]; then
- echo "Please run as root"
- sudo "$0" "$@"
- exit
-fi
-
echo "Resetting DevBits deployment database to a blank slate..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -20,7 +14,22 @@ if [[ ! -f ".env" ]]; then
exit 1
fi
-docker compose down -v --remove-orphans
+set -a
+. ./.env
+set +a
+
+if [[ -z "${DATABASE_URL:-}" ]]; then
+ echo "Missing DATABASE_URL in $ROOT_DIR/.env" >&2
+ exit 1
+fi
+
+if ! command -v psql >/dev/null 2>&1; then
+ echo "psql is required. Install PostgreSQL client tools first." >&2
+ exit 1
+fi
+
+psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "DROP SCHEMA IF EXISTS public CASCADE;"
+psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "CREATE SCHEMA public;"
if [[ "$KEEP_UPLOADS" != "--keep-uploads" ]]; then
if [[ -d "uploads" ]]; then
@@ -28,21 +37,15 @@ if [[ "$KEEP_UPLOADS" != "--keep-uploads" ]]; then
fi
fi
-docker compose up -d --build
+if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q '^devbits-api\.service'; then
+ echo "Restarting devbits-api service to recreate schema and warm startup..."
+ sudo systemctl restart devbits-api
+fi
echo "Database reset complete. All users and app data are removed."
-live_backend_state="unavailable"
-if backend_status_output="$(docker compose ps backend 2>/dev/null)"; then
- if echo "$backend_status_output" | grep -q "Up"; then
- live_backend_state="running"
- else
- live_backend_state="not running"
- fi
-fi
-
echo
echo "===== Summary ====="
echo "Action: Deployment DB reset executed"
-echo "Updated: Database recreated and services rebuilt"
-echo "Live backend: $live_backend_state"
+echo "Updated: Public schema recreated (blank state)"
+echo "Database reset target: DATABASE_URL"
diff --git a/backend/scripts/restore-deployment-db.sh b/backend/scripts/restore-deployment-db.sh
index d1e304b..85c4190 100644
--- a/backend/scripts/restore-deployment-db.sh
+++ b/backend/scripts/restore-deployment-db.sh
@@ -9,19 +9,23 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT_DIR"
-if [[ -f ".env" ]]; then
- set -a
- . ./.env
- set +a
+if [[ ! -f ".env" ]]; then
+ echo "Missing $ROOT_DIR/.env. Create it from backend/.env.example and set real values." >&2
+ exit 1
fi
-DB_USER="${POSTGRES_USER:-devbits}"
-DB_NAME="${POSTGRES_DB:-devbits}"
+set -a
+. ./.env
+set +a
+
+if [[ -z "${DATABASE_URL:-}" ]]; then
+ echo "Missing DATABASE_URL in $ROOT_DIR/.env" >&2
+ exit 1
+fi
-if [ "$EUID" -ne 0 ]; then
- echo "Please run as root"
- sudo "$0" "$@"
- exit
+if ! command -v psql >/dev/null 2>&1; then
+ echo "psql is required. Install PostgreSQL client tools first." >&2
+ exit 1
fi
if [[ ! -d "$BACKUP_DIR" ]]; then
@@ -39,10 +43,9 @@ RESOLVED_BACKUP="$LATEST_FILE"
echo "Using backup: $RESOLVED_BACKUP"
-docker compose exec -T db psql -U "$DB_USER" -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${DB_NAME}' AND pid <> pg_backend_pid();"
-docker compose exec -T db psql -U "$DB_USER" -d postgres -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";"
-docker compose exec -T db psql -U "$DB_USER" -d postgres -c "CREATE DATABASE \"${DB_NAME}\";"
-docker compose exec -T db psql -U "$DB_USER" -d "$DB_NAME" < "$RESOLVED_BACKUP"
+psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "DROP SCHEMA IF EXISTS public CASCADE;"
+psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "CREATE SCHEMA public;"
+psql "$DATABASE_URL" -v ON_ERROR_STOP=1 < "$RESOLVED_BACKUP"
db_file_name="$(basename "$RESOLVED_BACKUP")"
db_timestamp="${db_file_name#devbits-db-}"
@@ -65,22 +68,15 @@ else
echo "No matching uploads backup found for timestamp $db_timestamp, keeping current uploads directory."
fi
-echo "Restore complete. Rebuilding deployment services..."
-docker compose up -d --build
-
-echo "Restore complete and services rebuilt."
-
-live_backend_state="unavailable"
-if backend_status_output="$(docker compose ps backend 2>/dev/null)"; then
- if echo "$backend_status_output" | grep -q "Up"; then
- live_backend_state="running"
- else
- live_backend_state="not running"
- fi
+if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q '^devbits-api\.service'; then
+ echo "Restarting devbits-api service..."
+ sudo systemctl restart devbits-api
fi
+echo "Restore complete."
+
echo
echo "===== Summary ====="
echo "Action: Deployment DB restore executed"
-echo "Updated: Database restored and matching uploads restored when available; services rebuilt"
-echo "Live backend: $live_backend_state"
+echo "Updated: Database restored and matching uploads restored when available"
+echo "Database restore target: DATABASE_URL"
diff --git a/backend/scripts/update-live.ps1 b/backend/scripts/update-live.ps1
deleted file mode 100644
index d9056ca..0000000
--- a/backend/scripts/update-live.ps1
+++ /dev/null
@@ -1,108 +0,0 @@
-param(
- [switch]$NoPause
-)
-
-if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
- $arguments = "& '" + $myinvocation.mycommand.definition + "'"
- Start-Process powershell -Verb runAs -ArgumentList $arguments
- exit
-}
-
-$ErrorActionPreference = "Stop"
-
-Write-Host "Updating the live application by rebuilding and restarting the backend service..." -ForegroundColor Yellow
-
-$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
-$root = Resolve-Path (Join-Path $scriptDir "..")
-Push-Location $root
-
-try {
- $hostsPath = "C:\Windows\System32\drivers\etc\hosts"
- if (Test-Path $hostsPath) {
- $hostOverride = Select-String -Path $hostsPath -Pattern "(?im)^\s*127\.0\.0\.1\s+devbits\.ddns\.net(\s|$)" -ErrorAction SilentlyContinue
- if ($hostOverride) {
- Write-Host "WARNING: hosts file maps devbits.ddns.net to 127.0.0.1. This can cause domain/API connection failures and slow first loads." -ForegroundColor Yellow
- Write-Host "Remove that hosts override from $hostsPath (run editor as Administrator) if you want real domain routing." -ForegroundColor Yellow
- }
- }
-
- $envFile = Join-Path $root ".env"
- if (-not (Test-Path $envFile)) {
- throw "Missing $envFile. Create it from backend/.env.example and set strong credentials before deploying."
- }
-
- $envContent = Get-Content -Path $envFile -Raw
- if ($envContent -notmatch "(?m)^POSTGRES_PASSWORD=.+$") {
- throw "POSTGRES_PASSWORD is not set in $envFile."
- }
- if ($envContent -match "(?m)^POSTGRES_PASSWORD=(password|changeme|devbits)$") {
- throw "POSTGRES_PASSWORD in $envFile is weak/default. Set a strong random value before deploying."
- }
-
- # Ensure DB is started first so we can sync credentials to avoid auth mismatches.
- docker compose up -d db
-
- # Wait for DB health (up to ~60s)
- $dbHealthy = $false
- for ($attempt = 0; $attempt -lt 30; $attempt++) {
- $statusOutput = docker compose ps db 2>$null | Out-String
- if ($statusOutput -match "Up" -and $statusOutput -match "healthy") {
- $dbHealthy = $true
- break
- }
- Start-Sleep -Seconds 2
- }
-
- if (-not $dbHealthy) {
- Write-Host "Warning: DB did not reach healthy state in time; proceeding to attempt password sync anyway." -ForegroundColor Yellow
- }
-
- # Sync the POSTGRES_PASSWORD from .env into the Postgres role to keep credentials consistent.
- try {
- $envRaw = Get-Content -Path $envFile -Raw
- if ($envRaw -match "(?m)^POSTGRES_PASSWORD=(.+)$") {
- $dbPass = $Matches[1].Trim()
- if ($dbPass -and $dbPass -notmatch "^(password|changeme|devbits)$") {
- $tmpFile = Join-Path $env:TEMP "sync-devbits-password.sql"
- $safe = $dbPass.Replace("'", "''")
- Set-Content -Path $tmpFile -Value "ALTER ROLE devbits WITH PASSWORD '$safe';" -NoNewline
- try {
- Get-Content $tmpFile -Raw | docker compose exec -T db sh -lc "psql -U devbits -d postgres" | Out-Null
- Write-Host "Synchronized Postgres role password to match .env" -ForegroundColor Green
- }
- catch {
- Write-Host "Warning: Could not run password sync command inside DB container: $_" -ForegroundColor Yellow
- }
- Remove-Item $tmpFile -Force -ErrorAction SilentlyContinue
- }
- }
- }
- catch {
- Write-Host "Warning: Failed to read .env or sync password: $_" -ForegroundColor Yellow
- }
-
- docker compose up -d --build backend nginx
- Write-Host "Backend and nginx services have been updated." -ForegroundColor Green
-}
-finally {
- Pop-Location
-}
-
-$liveBackendState = "unavailable"
-try {
- $statusOutput = docker compose -f (Join-Path $root "docker-compose.yml") ps backend 2>$null
- if ($LASTEXITCODE -eq 0) {
- $liveBackendState = if (($statusOutput | Out-String) -match "Up") { "running" } else { "not running" }
- }
-}
-catch {}
-
-Write-Host ""
-Write-Host "===== Summary =====" -ForegroundColor Cyan
-Write-Host "Action: Live backend update executed"
-Write-Host "Updated: Backend rebuilt; nginx refreshed"
-Write-Host "Live backend: $liveBackendState"
-
-if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") {
- Read-Host "Press Enter to close"
-}
diff --git a/backend/scripts/update-live.sh b/backend/scripts/update-live.sh
index 8ae65b5..7c24965 100644
--- a/backend/scripts/update-live.sh
+++ b/backend/scripts/update-live.sh
@@ -1,53 +1,8 @@
-#!/bin/bash
+#!/usr/bin/env bash
+set -euo pipefail
-if [ "$EUID" -ne 0 ]; then
- echo "Please run as root"
- sudo "$0" "$@"
- exit
-fi
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
-set -e
-
-echo "Updating the live application by rebuilding and restarting the backend service..."
-
-# Get the directory of the script
-script_dir=$(dirname "$(readlink -f "$0")")
-root_dir=$(realpath "$script_dir/..")
-
-# Change to the root directory of the backend
-cd "$root_dir"
-
-env_file="$root_dir/.env"
-if [[ ! -f "$env_file" ]]; then
- echo "Missing $env_file. Create it from backend/.env.example and set strong credentials before deploying." >&2
- exit 1
-fi
-
-postgres_password="$(grep -E '^POSTGRES_PASSWORD=' "$env_file" | tail -n 1 | cut -d '=' -f2- || true)"
-if [[ -z "$postgres_password" ]]; then
- echo "POSTGRES_PASSWORD is not set in $env_file" >&2
- exit 1
-fi
-if [[ "$postgres_password" == "password" || "$postgres_password" == "changeme" || "$postgres_password" == "devbits" ]]; then
- echo "POSTGRES_PASSWORD in $env_file is weak/default. Set a strong random value before deploying." >&2
- exit 1
-fi
-
-docker compose up -d --build backend nginx
-
-echo "Backend and nginx services have been updated."
-
-live_backend_state="unavailable"
-if backend_status_output="$(docker compose ps backend 2>/dev/null)"; then
- if echo "$backend_status_output" | grep -q "Up"; then
- live_backend_state="running"
- else
- live_backend_state="not running"
- fi
-fi
-
-echo
-echo "===== Summary ====="
-echo "Action: Live backend update executed"
-echo "Updated: Backend rebuilt; nginx refreshed"
-echo "Live backend: $live_backend_state"
+echo "Updating live backend (native binary + systemd)..."
+"$ROOT_DIR/scripts/deploy-aws-native.sh"
diff --git a/backend/uploads/u1_1a5534ccca23297b1a89c65a.jpg b/backend/uploads/u1_1a5534ccca23297b1a89c65a.jpg
new file mode 100644
index 0000000..d760285
Binary files /dev/null and b/backend/uploads/u1_1a5534ccca23297b1a89c65a.jpg differ
diff --git a/frontend/DEEP_LINK_SETUP.md b/frontend/DEEP_LINK_SETUP.md
index d06c6ca..15f56da 100644
--- a/frontend/DEEP_LINK_SETUP.md
+++ b/frontend/DEEP_LINK_SETUP.md
@@ -55,18 +55,19 @@ Alternative (if you use Play App Signing)
4. Update `assetlinks.json`
-- Open `frontend/public/.well-known/assetlinks.json` and replace `` with the SHA-256 fingerprint (format: uppercase hex with colons or without; either is accepted by Android).
+- Open `backend/api/static/assetlinks.json` and replace `` with the SHA-256 fingerprint (format: uppercase hex with colons or without; either is accepted by Android).
-5. Deploy static files to your server
+5. Deploy static files to your server/domain
-- Build the frontend static output and copy contents of `frontend/public/` to the nginx static folder (`/usr/share/nginx/html`) so files are reachable at:
- - https://devbits.ddns.net/apple-app-site-association
- - https://devbits.ddns.net/.well-known/assetlinks.json
+- The Go backend now serves these files directly from `backend/api/static/`. No separate static hosting is needed.
+ - `https://devbits.app/apple-app-site-association` → served from `backend/api/static/apple-app-site-association` (also available at `/.well-known/apple-app-site-association`)
+ - `https://devbits.app/.well-known/assetlinks.json` → served from `backend/api/static/assetlinks.json`
+- To update these files, edit them in `backend/api/static/` and redeploy the backend.
6. Verify
- iOS: use Apple's AASA validator or check device logs when opening a Universal Link.
-- Android: open `https://devbits.ddns.net/.well-known/assetlinks.json` and use `adb shell am start -a android.intent.action.VIEW -d "https://devbits.ddns.net/some/path"` on a device with the app installed, or check Play Console URL handling tests.
+- Android: open `https://devbits.app/.well-known/assetlinks.json` and use `adb shell am start -a android.intent.action.VIEW -d "https://devbits.app/some/path"` on a device with the app installed, or check Play Console URL handling tests.
Notes
diff --git a/frontend/app.json b/frontend/app.json
index a0eb079..132bf87 100644
--- a/frontend/app.json
+++ b/frontend/app.json
@@ -21,7 +21,7 @@
"NSMicrophoneUsageDescription": "DevBits requests access to the microphone to record audio for posts or streams. Example: recording a short audio clip when creating content."
},
"associatedDomains": [
- "applinks:devbits.ddns.net"
+ "applinks:devbits.app"
]
},
"android": {
diff --git a/frontend/app/(tabs)/explore.tsx b/frontend/app/(tabs)/explore.tsx
index 3bfed92..7dca2be 100644
--- a/frontend/app/(tabs)/explore.tsx
+++ b/frontend/app/(tabs)/explore.tsx
@@ -104,7 +104,7 @@ export default function ExploreScreen() {
const requestGuard = useRequestGuard();
const reveal = useRef(new Animated.Value(0.08)).current;
const hasFocusedRef = useRef(false);
- const scrollRef = useRef(null);
+ const scrollRef = useRef(null);
const { scrollY, onScroll } = useTopBlurScroll();
useEffect(() => {
diff --git a/frontend/app/(tabs)/index.tsx b/frontend/app/(tabs)/index.tsx
index 4f4a3d9..f81030f 100644
--- a/frontend/app/(tabs)/index.tsx
+++ b/frontend/app/(tabs)/index.tsx
@@ -4,7 +4,9 @@ import {
InteractionManager,
Platform,
RefreshControl,
+ ScrollView,
StyleSheet,
+ ViewStyle,
View,
} from "react-native";
import {
@@ -74,7 +76,7 @@ export default function HomeScreen() {
const motion = useMotionConfig();
const hyprMotion = useHyprMotion();
const requestGuard = useRequestGuard();
- const scrollRef = useRef(null);
+ const scrollRef = useRef(null);
const hasFocusedRef = useRef(false);
const { scrollY, onScroll } = useTopBlurScroll();
const heroProgress = useSharedValue(0.08);
@@ -350,28 +352,28 @@ export default function HomeScreen() {
useAutoRefresh(() => loadFeed(false), { focusRefresh: false });
- const heroRevealStyle = useAnimatedStyle(() => ({
+ const heroRevealStyle = useAnimatedStyle(() => ({
opacity: heroProgress.value,
transform: [
{ translateY: (1 - heroProgress.value) * 22 },
{ scale: 0.97 + heroProgress.value * 0.03 },
- ],
+ ] as any,
}));
- const streamsRevealStyle = useAnimatedStyle(() => ({
+ const streamsRevealStyle = useAnimatedStyle(() => ({
opacity: streamsProgress.value,
transform: [
{ translateY: (1 - streamsProgress.value) * 22 },
{ scale: 0.97 + streamsProgress.value * 0.03 },
- ],
+ ] as any,
}));
- const postsRevealStyle = useAnimatedStyle(() => ({
+ const postsRevealStyle = useAnimatedStyle(() => ({
opacity: postsProgress.value,
transform: [
{ translateY: (1 - postsProgress.value) * 22 },
{ scale: 0.97 + postsProgress.value * 0.03 },
- ],
+ ] as any,
}));
const cursorStyle = useAnimatedStyle(() => ({
diff --git a/frontend/app/bytes.tsx b/frontend/app/bytes.tsx
index 430ecfc..1ad977f 100644
--- a/frontend/app/bytes.tsx
+++ b/frontend/app/bytes.tsx
@@ -487,6 +487,12 @@ const styles = StyleSheet.create({
paddingTop: 8,
flexWrap: "wrap",
},
+ filterRow: {
+ flexDirection: "row",
+ gap: 8,
+ paddingTop: 8,
+ flexWrap: "wrap",
+ },
filterChip: {
paddingVertical: 6,
paddingHorizontal: 10,
diff --git a/frontend/app/post/[postId].tsx b/frontend/app/post/[postId].tsx
index eaf99d5..0562245 100644
--- a/frontend/app/post/[postId].tsx
+++ b/frontend/app/post/[postId].tsx
@@ -14,6 +14,7 @@ import {
Platform,
Pressable,
RefreshControl,
+ ScrollView,
StyleSheet,
TextInput,
TouchableWithoutFeedback,
@@ -97,7 +98,7 @@ export default function PostDetailScreen() {
const { isSaved, savedPostIds, toggleSave } = useSaved();
const motion = useMotionConfig();
const reveal = useRef(new Animated.Value(0.08)).current;
- const scrollRef = useRef(null);
+ const scrollRef = useRef(null);
const { scrollY, onScroll } = useTopBlurScroll();
const [post, setPost] = useState(null);
const [project, setProject] = useState(null);
diff --git a/frontend/app/saved-streams.tsx b/frontend/app/saved-streams.tsx
index 21223cf..bc1e84d 100644
--- a/frontend/app/saved-streams.tsx
+++ b/frontend/app/saved-streams.tsx
@@ -1,5 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
-import { Animated, RefreshControl, StyleSheet, View } from "react-native";
+import {
+ Animated,
+ RefreshControl,
+ ScrollView,
+ StyleSheet,
+ View,
+} from "react-native";
import {
SafeAreaView,
useSafeAreaInsets,
@@ -35,7 +41,7 @@ export default function SavedStreamsScreen() {
);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
- const scrollRef = useRef(null);
+ const scrollRef = useRef(null);
const motion = useMotionConfig();
const reveal = useRef(new Animated.Value(0.08)).current;
const { scrollY, onScroll } = useTopBlurScroll();
diff --git a/frontend/app/settings/about.tsx b/frontend/app/settings/about.tsx
index 0fdcb4f..8b4be32 100644
--- a/frontend/app/settings/about.tsx
+++ b/frontend/app/settings/about.tsx
@@ -8,7 +8,7 @@ import { useAppColors } from "@/hooks/useAppColors";
import { SettingsPageShell, settingsStyles } from "@/features/settings/shared";
const SITE_BASE_URL = (
- process.env.EXPO_PUBLIC_SITE_URL?.trim() || "https://devbits.ddns.net"
+ process.env.EXPO_PUBLIC_SITE_URL?.trim() || "https://devbits.app"
).replace(/\/+$/, "");
const publicLinks = [
diff --git a/frontend/app/settings/index.tsx b/frontend/app/settings/index.tsx
index 05341ba..eb1b22f 100644
--- a/frontend/app/settings/index.tsx
+++ b/frontend/app/settings/index.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from "react";
-import { Animated, StyleSheet, View } from "react-native";
+import { Animated, ScrollView, StyleSheet, View } from "react-native";
import {
SafeAreaView,
useSafeAreaInsets,
@@ -20,7 +20,7 @@ export default function SettingsHubScreen() {
const router = useRouter();
const motion = useMotionConfig();
const reveal = useRef(new Animated.Value(0.08)).current;
- const scrollRef = useRef(null);
+ const scrollRef = useRef(null);
const { scrollY, onScroll } = useTopBlurScroll();
const { user } = useAuth();
diff --git a/frontend/app/stream/[projectId].tsx b/frontend/app/stream/[projectId].tsx
index feac932..5365d8d 100644
--- a/frontend/app/stream/[projectId].tsx
+++ b/frontend/app/stream/[projectId].tsx
@@ -9,6 +9,7 @@ import {
Animated,
Pressable,
RefreshControl,
+ ScrollView,
StyleSheet,
View,
} from "react-native";
@@ -108,7 +109,7 @@ export default function StreamDetailScreen() {
const bottom = useBottomTabOverflow();
const motion = useMotionConfig();
const reveal = useRef(new Animated.Value(0.08)).current;
- const scrollRef = useRef(null);
+ const scrollRef = useRef(null);
const { scrollY, onScroll } = useTopBlurScroll();
const hasLoadedOnceRef = useRef(false);
const loadSequenceRef = useRef(0);
diff --git a/frontend/app/user/[username].tsx b/frontend/app/user/[username].tsx
index 7d3fe7a..768b875 100644
--- a/frontend/app/user/[username].tsx
+++ b/frontend/app/user/[username].tsx
@@ -11,6 +11,7 @@ import {
Modal,
Pressable,
RefreshControl,
+ ScrollView,
StyleSheet,
TextInput,
View,
@@ -87,7 +88,7 @@ export default function UserProfileScreen() {
const motion = useMotionConfig();
const reveal = useRef(new Animated.Value(0.08)).current;
const hasLoadedRef = useRef(false);
- const scrollRef = useRef(null);
+ const scrollRef = useRef(null);
const { scrollY, onScroll } = useTopBlurScroll();
const filteredFollowerUsers = useMemo(() => {
diff --git a/frontend/components/FadeInImage.tsx b/frontend/components/FadeInImage.tsx
index 818b1d5..8d2a94c 100644
--- a/frontend/components/FadeInImage.tsx
+++ b/frontend/components/FadeInImage.tsx
@@ -148,9 +148,9 @@ export function FadeInImage({
onError?.(event);
};
- const handleLoadEnd: ImageProps["onLoadEnd"] = (event) => {
+ const handleLoadEnd: ImageProps["onLoadEnd"] = () => {
reveal();
- onLoadEnd?.(event);
+ onLoadEnd?.();
};
if (loadFailed) {
diff --git a/frontend/components/HyprBackdrop.tsx b/frontend/components/HyprBackdrop.tsx
index c4f3303..6317020 100644
--- a/frontend/components/HyprBackdrop.tsx
+++ b/frontend/components/HyprBackdrop.tsx
@@ -34,11 +34,13 @@ export function HyprBackdrop() {
});
const baseColors =
- scheme === "dark" ? ["#0a1110", "#0e1715"] : ["#f2f7f3", "#e6f2eb"];
+ scheme === "dark"
+ ? (["#0a1110", "#0e1715"] as const)
+ : (["#f2f7f3", "#e6f2eb"] as const);
const glowColors =
scheme === "dark"
- ? ["rgba(58, 227, 186, 0.18)", "rgba(51, 184, 255, 0.12)"]
- : ["rgba(61, 211, 176, 0.22)", "rgba(77, 176, 255, 0.18)"];
+ ? (["rgba(58, 227, 186, 0.18)", "rgba(51, 184, 255, 0.12)"] as const)
+ : (["rgba(61, 211, 176, 0.22)", "rgba(77, 176, 255, 0.18)"] as const);
return (
diff --git a/frontend/components/TopBlur.tsx b/frontend/components/TopBlur.tsx
index ecfffbd..a58dc08 100644
--- a/frontend/components/TopBlur.tsx
+++ b/frontend/components/TopBlur.tsx
@@ -15,15 +15,19 @@ export function TopBlur({ scrollY }: TopBlurProps) {
const height = Math.max(insets.top, 12) + 28;
const veilColors = useMemo(() => {
if (theme === "dark") {
- return ["rgba(0, 0, 0, 0.24)", "rgba(0, 0, 0, 0.12)", "rgba(0, 0, 0, 0)"];
+ return [
+ "rgba(0, 0, 0, 0.24)",
+ "rgba(0, 0, 0, 0.12)",
+ "rgba(0, 0, 0, 0)",
+ ] as const;
}
return [
"rgba(255, 255, 255, 0.2)",
"rgba(255, 255, 255, 0.1)",
"rgba(255, 255, 255, 0)",
- ];
+ ] as const;
}, [theme]);
- const veilStops = [0, 0.5, 1];
+ const veilStops = [0, 0.5, 1] as const;
const opacity = scrollY
? scrollY.interpolate({
inputRange: [0, 60],
diff --git a/frontend/components/__tests__/ThemedText-test.tsx b/frontend/components/__tests__/ThemedText-test.tsx
index 2a11b4d..8870e98 100644
--- a/frontend/components/__tests__/ThemedText-test.tsx
+++ b/frontend/components/__tests__/ThemedText-test.tsx
@@ -4,9 +4,33 @@ import renderer from "react-test-renderer";
import { ThemedText } from "../ThemedText";
+jest.mock("@/contexts/PreferencesContext", () => ({
+ usePreferences: () => ({
+ preferences: {
+ zenMode: false,
+ compactMode: false,
+ textRenderEffect: "off",
+ },
+ }),
+}));
+
+jest.mock("@/hooks/useThemeColor", () => ({
+ useThemeColor: () => "#000",
+}));
+
it("renders correctly", () => {
- const testRenderer = renderer.create(Snapshot test!);
+ let testRenderer: renderer.ReactTestRenderer;
+ renderer.act(() => {
+ testRenderer = renderer.create(
+
+ Snapshot test!
+ ,
+ );
+ });
- const textNode = testRenderer.root.findByType(Text);
+ const textNode = testRenderer!.root.findByType(Text);
expect(textNode.props.children).toBe("Snapshot test!");
+ renderer.act(() => {
+ testRenderer!.unmount();
+ });
});
diff --git a/frontend/eas.json b/frontend/eas.json
index aee494a..763fcd5 100644
--- a/frontend/eas.json
+++ b/frontend/eas.json
@@ -14,9 +14,9 @@
"production": {
"autoIncrement": true,
"env": {
- "EXPO_PUBLIC_API_URL": "https://devbits.ddns.net",
- "EXPO_PUBLIC_API_FALLBACK_URL": "https://devbits.ddns.net",
- "EXPO_PUBLIC_SITE_URL": "https://devbits.ddns.net"
+ "EXPO_PUBLIC_API_URL": "https://devbits.app",
+ "EXPO_PUBLIC_API_FALLBACK_URL": "https://devbits.app",
+ "EXPO_PUBLIC_SITE_URL": "https://devbits.app"
}
,
"credentialsSource": "remote"
diff --git a/frontend/public/account-deletion.html b/frontend/public/account-deletion.html
index 12e5725..8828e83 100644
--- a/frontend/public/account-deletion.html
+++ b/frontend/public/account-deletion.html
@@ -237,7 +237,7 @@
How to request deletion
Email us at
- mail@elifouts.com with the
+ mail@elifouts.net with the
subject "Account Deletion Request" and include the username and the
email address associated with your account.
privacy and to handling personal information in a responsible manner.
This policy describes how we collect, use, disclose, and protect
personal information through our website
- devbits.ddns.net, our mobile
+ devbits.app, our mobile
applications (Devbits on Android and iOS), and related services.
diff --git a/frontend/services/api.ts b/frontend/services/api.ts
index b31fe29..899fdc0 100644
--- a/frontend/services/api.ts
+++ b/frontend/services/api.ts
@@ -214,7 +214,7 @@ const getHostFromUri = (uri?: string | null) => {
const getDefaultBaseUrl = () => {
// Check for overrides provided via Expo `extra` or environment variables.
// This allows running the Expo client locally while targeting a remote
- // backend (for example devbits.ddns.net) from any developer machine.
+ // backend (for example devbits.app) from any developer machine.
try {
const extras = (Constants as any)?.expoConfig?.extra ??
(Constants as any)?.manifest2?.extra ??
@@ -257,9 +257,9 @@ const getDefaultBaseUrl = () => {
}
// If the developer explicitly disables using the local API in dev, use
- // the production DDNS endpoint even when __DEV__ is true.
+ // the production endpoint even when __DEV__ is true.
if (!useLocal) {
- return "https://devbits.ddns.net";
+ return "https://devbits.app";
}
} catch {
// ignore and fall back to defaults below
@@ -294,7 +294,7 @@ const getDefaultBaseUrl = () => {
}
// Production: use live server
- return "https://devbits.ddns.net";
+ return "https://devbits.app";
};
const normalizeBaseUrl = (url: string) => url.replace(/\/+$/, "");
@@ -318,30 +318,30 @@ const buildBaseUrlList = (...candidates: Array) => {
return urls;
};
-export const API_BASE_URL = normalizeBaseUrl(getDefaultBaseUrl());
-
-// Defensive runtime validation: if the resolved URL is malformed (e.g. "http://"
-// with no host) fall back to the public DDNS host and log an error so developers
-// can see the problem in the Metro/Expo console.
-try {
- const checkUrl = new URL(API_BASE_URL);
- if (!checkUrl.hostname) {
- // eslint-disable-next-line no-console
- console.error("Invalid API_BASE_URL resolved; falling back to https://devbits.ddns.net", API_BASE_URL);
- // normalize and overwrite
- (exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.ddns.net");
- }
-} catch (e) {
- // If parsing fails entirely, fallback and log.
+// Validate the resolved URL at module load time; if it is malformed (e.g.
+// "http://" with no host) fall back to the public host and log an error so
+// developers can see the problem in the Metro/Expo console.
+function getValidatedBaseUrl(): string {
+ const raw = normalizeBaseUrl(getDefaultBaseUrl());
try {
+ const checkUrl = new URL(raw);
+ if (!checkUrl.hostname) {
+ // eslint-disable-next-line no-console
+ console.error("Invalid API_BASE_URL resolved; falling back to https://devbits.app", raw);
+ return normalizeBaseUrl("https://devbits.app");
+ }
+ return raw;
+ } catch (e) {
// eslint-disable-next-line no-console
- console.error("Failed to parse API_BASE_URL; falling back to https://devbits.ddns.net", API_BASE_URL, String(e));
- } catch {}
- (exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.ddns.net");
+ console.error("Failed to parse API_BASE_URL; falling back to https://devbits.app", raw, String(e));
+ return normalizeBaseUrl("https://devbits.app");
+ }
}
+export const API_BASE_URL = getValidatedBaseUrl();
+
const API_FALLBACK_URL = normalizeBaseUrl(
- __DEV__ ? "" : "https://devbits.ddns.net",
+ __DEV__ ? "" : "https://devbits.app",
);
const API_REQUEST_BASE_URLS = buildBaseUrlList(API_BASE_URL, API_FALLBACK_URL);
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 8466a87..7164bec 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -1,14 +1,12 @@
{
+ "extends": "expo/tsconfig.base",
"compilerOptions": {
- "ignoreDeprecations": "6.0",
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
- "moduleResolution": "bundler",
- "types": ["jest", "react", "react-native"]
+ "types": ["jest"]
},
- "extends": "expo/tsconfig.base",
"include": [
"**/*.ts",
"**/*.tsx",