+ 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.com 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/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..131da44
--- /dev/null
+++ b/backend/docs/AWS_TRANSFER_NO_NGINX.md
@@ -0,0 +1,83 @@
+# 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 (Ubuntu 24.04 LTS).
+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 Go (`1.24.x`) and git
+ - Clone repo to `/opt/devbits`
+ - `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
+```
+
+## 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/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..e0666ce 100644
--- a/backend/scripts/README.md
+++ b/backend/scripts/README.md
@@ -1,55 +1,25 @@
-# DevBits Database Scripts
+# DevBits Backend Scripts
-All deployment database scripts are in this folder.
+Run scripts from `backend/`.
-## Environment separation (important)
+## Native AWS deploy scripts (recommended)
-### Local DB (development machine)
+- `scripts/build-backend-linux.sh`
+ - Builds `bin/devbits-api` for Linux.
+- `scripts/install-aws-systemd-service.sh`
+ - Installs/restarts `devbits-api` systemd service.
+- `scripts/deploy-aws-native.sh`
+ - Build + install/restart service in one command.
+- `scripts/update-live.sh`
+ - Wrapper around native deploy script.
+- `scripts/update-live.ps1`
+ - Runs the Linux deploy script remotely over SSH from Windows.
-Run from project root:
+See: `backend/docs/AWS_TRANSFER_NO_NGINX.md`
-```powershell
-cd c:\Users\eligf\DevBits
-```
+## Database backup/reset scripts (Docker-based)
-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:
-
-```bash
-cd /path/to/DevBits/backend
-docker compose up -d
-docker compose logs -f db
-```
-
-Only run reset/restore in the environment you mean to modify.
-
-## Script location
-
-Run script commands from `backend`:
-
-```powershell
-cd backend
-```
-
-## Required env file
-
-Before running deploy/reset/update scripts, ensure `backend/.env` exists:
-
-```powershell
-Copy-Item .env.example .env
-```
-
-Set a strong `POSTGRES_PASSWORD` value in `.env`.
-
-## Scripts
+These scripts are for environments where Postgres runs via `docker compose`:
- `scripts/reset-deployment-db.ps1` / `scripts/reset-deployment-db.sh`
- `scripts/backup-deployment-db.ps1` / `scripts/backup-deployment-db.sh`
@@ -57,109 +27,4 @@ Set a strong `POSTGRES_PASSWORD` value in `.env`.
- `scripts/setup-daily-backup-task.ps1`
- `scripts/disable-daily-backup-task.ps1`
-## 1) Reset DB (blank slate)
-
-Warning: this wipes all app data in that environment.
-
-PowerShell:
-
-```powershell
-./scripts/reset-deployment-db.ps1
-```
-
-Keep uploads while resetting only DB volume:
-
-```powershell
-./scripts/reset-deployment-db.ps1 -KeepUploads
-```
-
-Bash:
-
-```bash
-./scripts/reset-deployment-db.sh
-./scripts/reset-deployment-db.sh --keep-uploads
-```
-
-## 2) Backup DB (single-backup retention)
-
-Safe for both local and live. Run it in the target environment.
-
-PowerShell:
-
-```powershell
-./scripts/backup-deployment-db.ps1
-```
-
-Bash:
-
-```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
-
-Warning: restore terminates sessions and recreates DB in that environment.
-
-PowerShell:
-
-```powershell
-./scripts/restore-deployment-db.ps1
-```
-
-Bash:
-
-```bash
-./scripts/restore-deployment-db.sh
-```
-
-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:
-
-```powershell
-./scripts/setup-daily-backup-task.ps1
-```
-
-Custom time:
-
-```powershell
-./scripts/setup-daily-backup-task.ps1 -RunAt "01:30"
-```
-
-Notes:
-
-- Script tries `SYSTEM` first.
-- If shell is not elevated, it falls back to current-user mode.
-
-Verify task:
-
-```powershell
-schtasks /Query /TN DevBitsDailyDbBackup /V /FO LIST
-```
-
-## 5) Disable daily auto backup (Windows)
-
-```powershell
-./scripts/disable-daily-backup-task.ps1
-```
+If production uses RDS/native Postgres, use `pg_dump`/`psql` against RDS instead of these Docker-targeted scripts.
diff --git a/backend/scripts/build-backend-linux.sh b/backend/scripts/build-backend-linux.sh
new file mode 100644
index 0000000..371a4fe
--- /dev/null
+++ b/backend/scripts/build-backend-linux.sh
@@ -0,0 +1,18 @@
+#!/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}"
+TARGET_GOARCH="${TARGET_GOARCH:-amd64}"
+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..8a772b0
--- /dev/null
+++ b/backend/scripts/install-aws-systemd-service.sh
@@ -0,0 +1,55 @@
+#!/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:-ubuntu}}"
+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 ! 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/update-live.ps1 b/backend/scripts/update-live.ps1
index d9056ca..250e6aa 100644
--- a/backend/scripts/update-live.ps1
+++ b/backend/scripts/update-live.ps1
@@ -1,107 +1,23 @@
param(
+ [Parameter(Mandatory = $true)]
+ [string]$ServerHost,
+ [string]$User = "ubuntu",
+ [string]$ProjectPath = "/opt/devbits/backend",
[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."
- }
+Write-Host "Deploying backend on AWS host $User@$ServerHost using native Linux deploy script..." -ForegroundColor Yellow
- # 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
-}
+$remoteCommand = "cd $ProjectPath && ./scripts/deploy-aws-native.sh"
+ssh "$User@$ServerHost" $remoteCommand
-$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" }
- }
+if ($LASTEXITCODE -ne 0) {
+ throw "Remote deploy failed with exit code $LASTEXITCODE"
}
-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"
+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/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/frontend/DEEP_LINK_SETUP.md b/frontend/DEEP_LINK_SETUP.md
index d06c6ca..4e72f8e 100644
--- a/frontend/DEEP_LINK_SETUP.md
+++ b/frontend/DEEP_LINK_SETUP.md
@@ -57,16 +57,16 @@ Alternative (if you use Play App Signing)
- 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).
-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
+- Host `frontend/public/apple-app-site-association` and `frontend/public/.well-known/assetlinks.json` at:
+ - https://devbits.app/apple-app-site-association
+ - https://devbits.app/.well-known/assetlinks.json
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/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/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/privacy-policy.html b/frontend/public/privacy-policy.html
index 845d695..0227e39 100644
--- a/frontend/public/privacy-policy.html
+++ b/frontend/public/privacy-policy.html
@@ -222,7 +222,7 @@
Introduction & Organizational Information
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..57018e8 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(/\/+$/, "");
@@ -321,27 +321,27 @@ const buildBaseUrlList = (...candidates: Array) => {
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
+// with no host) fall back to the public 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);
+ console.error("Invalid API_BASE_URL resolved; falling back to https://devbits.app", API_BASE_URL);
// normalize and overwrite
- (exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.ddns.net");
+ (exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.app");
}
} catch (e) {
// If parsing fails entirely, fallback and log.
try {
// 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));
+ console.error("Failed to parse API_BASE_URL; falling back to https://devbits.app", API_BASE_URL, String(e));
} catch {}
- (exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.ddns.net");
+ (exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.app");
}
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);
From 4a2caa75d027c1c844b576c1bcceecb809385c85 Mon Sep 17 00:00:00 2001
From: Eli Fouts
Date: Fri, 6 Mar 2026 21:48:37 -0500
Subject: [PATCH 02/10] Update backend/api/static/account-deletion.html
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
backend/api/static/account-deletion.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/api/static/account-deletion.html b/backend/api/static/account-deletion.html
index 12e5725..8828e83 100644
--- a/backend/api/static/account-deletion.html
+++ b/backend/api/static/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.
From bcc2eb0bcff81aa80df46f300951a0121331e11d Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Mar 2026 22:05:17 -0500
Subject: [PATCH 03/10] Fix review comments: AASA content-type, proxy header
spoofing, API_BASE_URL const mutation, installer guard, docs (#143)
* Initial plan
* Address PR review comments: AASA content-type, proxy header security, template check, API_BASE_URL fix, docs update
Co-authored-by: elifouts <116454864+elifouts@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: elifouts <116454864+elifouts@users.noreply.github.com>
---
backend/.env.example | 4 +++
backend/api/internal/handlers/media_routes.go | 19 ++++++----
backend/api/main.go | 14 ++++++--
backend/go.mod | 2 +-
.../scripts/install-aws-systemd-service.sh | 6 ++++
frontend/DEEP_LINK_SETUP.md | 9 ++---
frontend/services/api.ts | 36 +++++++++----------
7 files changed, 59 insertions(+), 31 deletions(-)
diff --git a/backend/.env.example b/backend/.env.example
index 090bad1..3fe41c9 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -11,3 +11,7 @@ DEVBITS_JWT_SECRET=replace-with-64+char-random-secret
DEVBITS_ADMIN_KEY=replace-with-base64-or-random-admin-key
DEVBITS_ADMIN_LOCAL_ONLY=0
DEVBITS_API_ADDR=0.0.0.0:8080
+# Set to "true" when running behind a trusted reverse proxy (e.g. AWS ALB/nginx)
+# that sets the X-Forwarded-Proto header. Do NOT enable if the backend is directly
+# internet-facing, as the header would be client-controlled.
+DEVBITS_TRUST_PROXY=false
diff --git a/backend/api/internal/handlers/media_routes.go b/backend/api/internal/handlers/media_routes.go
index 3b886a5..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": {},
@@ -207,13 +212,15 @@ func UploadMedia(context *gin.Context) {
}
scheme := "http"
- 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"
- }
- } else if context.Request.TLS != nil {
+ 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)
diff --git a/backend/api/main.go b/backend/api/main.go
index cfb40b7..a90cf78 100644
--- a/backend/api/main.go
+++ b/backend/api/main.go
@@ -195,8 +195,18 @@ func main() {
} else {
log.Printf("INFO: admin UI available at /admin (key-protected)")
}
- router.StaticFile("/apple-app-site-association", "./api/static/apple-app-site-association")
- router.StaticFile("/.well-known/assetlinks.json", "./api/static/assetlinks.json")
+ 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")
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/scripts/install-aws-systemd-service.sh b/backend/scripts/install-aws-systemd-service.sh
index 8a772b0..96db2ad 100644
--- a/backend/scripts/install-aws-systemd-service.sh
+++ b/backend/scripts/install-aws-systemd-service.sh
@@ -30,6 +30,12 @@ if [[ ! -x "$BINARY_PATH" ]]; then
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
diff --git a/frontend/DEEP_LINK_SETUP.md b/frontend/DEEP_LINK_SETUP.md
index 4e72f8e..15f56da 100644
--- a/frontend/DEEP_LINK_SETUP.md
+++ b/frontend/DEEP_LINK_SETUP.md
@@ -55,13 +55,14 @@ 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/domain
-- Host `frontend/public/apple-app-site-association` and `frontend/public/.well-known/assetlinks.json` at:
- - https://devbits.app/apple-app-site-association
- - https://devbits.app/.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
diff --git a/frontend/services/api.ts b/frontend/services/api.ts
index 57018e8..899fdc0 100644
--- a/frontend/services/api.ts
+++ b/frontend/services/api.ts
@@ -318,28 +318,28 @@ 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 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.app", API_BASE_URL);
- // normalize and overwrite
- (exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.app");
- }
-} 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.app", API_BASE_URL, String(e));
- } catch {}
- (exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.app");
+ 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.app",
);
From f593ab139e5f2ac046c387a4bee4971405df27d3 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Mar 2026 22:05:51 -0500
Subject: [PATCH 04/10] Consolidate static/compliance files to single source of
truth with sync script (#144)
* Initial plan
* Add sync-static.sh to prevent static file drift; fix account-deletion.html email
Co-authored-by: grillinr <169214325+grillinr@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: grillinr <169214325+grillinr@users.noreply.github.com>
---
Bash-Scripts/sync-static.sh | 38 +++++++++++++++++++++++++++
README.md | 9 +++++++
frontend/public/account-deletion.html | 2 +-
3 files changed, 48 insertions(+), 1 deletion(-)
create mode 100755 Bash-Scripts/sync-static.sh
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/README.md b/README.md
index 4ee1967..a502674 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,15 @@ 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:
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.
From 764d832358f702e160907b273b48c8b2b00207d4 Mon Sep 17 00:00:00 2001
From: Eli
Date: Sun, 8 Mar 2026 23:47:27 -0400
Subject: [PATCH 05/10] Refactor scripts and components for improved clarity
and functionality; update TypeScript configurations and enhance local
development experience.
---
Bash-Scripts/run-dev.sh | 22 ++++---
Bash-Scripts/run-front.sh | 55 +++++++++++++++---
backend/api/internal/database/dev.sqlite3-shm | Bin 32768 -> 32768 bytes
backend/api/internal/database/dev.sqlite3-wal | Bin 98912 -> 173072 bytes
backend/bin/api | Bin 0 -> 22037576 bytes
.../uploads/u1_1a5534ccca23297b1a89c65a.jpg | Bin 0 -> 58053 bytes
frontend/app/(tabs)/explore.tsx | 2 +-
frontend/app/(tabs)/index.tsx | 16 ++---
frontend/app/bytes.tsx | 6 ++
frontend/app/post/[postId].tsx | 3 +-
frontend/app/saved-streams.tsx | 10 +++-
frontend/app/settings/index.tsx | 4 +-
frontend/app/stream/[projectId].tsx | 3 +-
frontend/app/user/[username].tsx | 3 +-
frontend/components/FadeInImage.tsx | 4 +-
frontend/components/HyprBackdrop.tsx | 8 ++-
frontend/components/TopBlur.tsx | 10 +++-
.../components/__tests__/ThemedText-test.tsx | 28 ++++++++-
frontend/tsconfig.json | 6 +-
19 files changed, 134 insertions(+), 46 deletions(-)
create mode 100644 backend/bin/api
create mode 100644 backend/uploads/u1_1a5534ccca23297b1a89c65a.jpg
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 d487864..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.app) 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" ;;
@@ -74,13 +101,25 @@ 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.app"
+ echo "Using live backend: https://devbits.app"
fi
cd "$FRONTEND_DIR"
diff --git a/backend/api/internal/database/dev.sqlite3-shm b/backend/api/internal/database/dev.sqlite3-shm
index f70b4c0b186edbbccdfc49ad487eeebcc9e90c8d..0419e7c090960e2fd45f7fac369756edf5fa5e3e 100644
GIT binary patch
delta 297
zcmZo@U}|V!s+V}A%K!qbK+MR%AfN@L-GJC6i6h|&N7SkUjTsi#YP^=;{3oUPt+-o{
zRP{ikz(5^H{zn2(;feK`u0U%UjDXk?h+%9eAln(r7YEW(Knzmj0u=+XU6I&s8$Sj*
zGD=VUSU>rnlK`hAgA9WLgDQi@#*e!h85JjfT*)Z6@#9}6MtLx)vhiau6QdGHSRE{&
hweh1m%j7ws0-RC|vJA=$Y7CkiKi(AC+?Q1(3IK+NO=AE6
delta 169
zcmZo@U}|V!s+V}A%K!t63=9GiK#l_t?<@_hQh01%ij6^$A5HNp@TLJWxk11X9zF}fHdMpPD2gE52{BN{YHFmVZn_`cV>YYQ{#A0|${
zWV`o$pZ9(HKHvBAywAN?CerqwQ|vv1_e8<`{!H1#mZQVtGy6Nh-m<)`Wf+E8;UE7j
z2PewP@nx|qWv5iCZ9g6Ur0yx(6bY?*f;`~x;xz<8#-=kr!0V%ida1Uc0H2UYJl=rc
z;~QXt!(snWI8n0NowU_OQ!cA*JxwLrDI+-Q&}iD7&8=2v7qyP=GAk(f^bdK0;Fcpd
zTc}0+KDT#-YHoAUO|;Vt@M@cmH8wh&baPW18cP|`xOh+Ct}Re(pj}kk{p;5&OlNoc
z@r&t8k%QJj>zu@?Ly8Zs`uN2=B9@ktM>2a>|;35NN%Ekz5y6)Ito=Rq~{
zOIf#u{GM>R}hLZ9E>mlV5wDulZK))b_{1pl8J2&Fnvf9PlR$c
zx6&J2*4B1bzq~v36J|j2WhUfXb#I#1t_RyIvpe|(yDfYP=_EsrFCq|ZXuUiLn#S45
zyEe@z5eP)42Ki&>ZJT3?ljj;|$k?g&Oc*iBJ=3*ab5Vhj1~d=%m<2`-shmdGs8yqX
z$jZ+)8|>I~qIOjashY>s*mIalta_7tKjTuyE9oS8Q1PVlBiXE?J8e!Dl{h4Kiyjv_
zh;;l@*wL$+pPpL@bgOGis?<_#aWOt&Vd)wi3T^ZEMgq)({%~?x1FkF7QnnePr7Q*-
zWg&Q>j#Q&5pk%S!wMUtA>$K{7{a5=3swn-AjV;xn+
z^6robdc$rueCH;+)@JR0W4e0gXHvWyQxkod*p~5j#yGi!ELC1tzODd@9@%->l;nHK
zY4Ma;B04A9F47Y35PkR;xCgt6`KIU0vCMS@5p%bGi9<(gka+6bJ&n~lHsiKQijrze
zO7MN7tOmhQgy~~?JrO1p45w6zQerL11ok>;v8Oi@0v>~Y)ySZqdcf+m-)D6iE6YrY
zfFs!u84QIZP^HA-qf%yrvWgsVO_C1I8Vp%5SX1K*>bWsK{aW!j$dElEAr+=N!_y9(
z-^UEW{TlFx!=8SB*lkIz7mjD;OSL8wKDn87E!CP@D@buJ#Rg{A6pO?Dpbs3C>eJv4
zGJ^~lljegId@R1s!FC3&6-??pR#C`|L#w>#YyQPq%=S*-(pSER_gH)0aAXyctrU+4c=6D!};nQeQL_7Jdn$hDfG)?2EITEnPQ1e=SaZ@Y@^D9~n7h5U6iNY;w=a
zKc=3^J>KqI^4KjzeeMF(XPik~P|&RZQ~wXYxw2<7{sW}`JG^WRvq`n}dVH+>cesb2
zwagCT>}o{pZ%wctw=TrLnG$Y_W{SHBvbRjrq7F0^JdJ#y^~k&xFL%qnJ_fIIzX8V$
BO27aB
delta 11
ScmbPmg6lyGTf-K{geCwVx&-6^
diff --git a/backend/bin/api b/backend/bin/api
new file mode 100644
index 0000000000000000000000000000000000000000..b94c784ce749eb15eb72fbd1987ea74858ba660b
GIT binary patch
literal 22037576
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