diff --git a/.sqlx/query-076cbf7f32c5f0103207a8e0e73dd5768681ff2520682edda8f2977dcae7cd62.json b/.sqlx/query-076cbf7f32c5f0103207a8e0e73dd5768681ff2520682edda8f2977dcae7cd62.json
deleted file mode 100644
index f23a720..0000000
--- a/.sqlx/query-076cbf7f32c5f0103207a8e0e73dd5768681ff2520682edda8f2977dcae7cd62.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity')",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Text"
- ]
- },
- "nullable": []
- },
- "hash": "076cbf7f32c5f0103207a8e0e73dd5768681ff2520682edda8f2977dcae7cd62"
-}
diff --git a/.sqlx/query-1658a90aede20695b0e6e87d2536fad5a538dbfc442625ef306272d2530ddc3a.json b/.sqlx/query-1658a90aede20695b0e6e87d2536fad5a538dbfc442625ef306272d2530ddc3a.json
deleted file mode 100644
index a7f927b..0000000
--- a/.sqlx/query-1658a90aede20695b0e6e87d2536fad5a538dbfc442625ef306272d2530ddc3a.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "SELECT handle, email FROM users WHERE did = $1",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "handle",
- "type_info": "Text"
- },
- {
- "ordinal": 1,
- "name": "email",
- "type_info": "Text"
- }
- ],
- "parameters": {
- "Left": [
- "Text"
- ]
- },
- "nullable": [
- false,
- true
- ]
- },
- "hash": "1658a90aede20695b0e6e87d2536fad5a538dbfc442625ef306272d2530ddc3a"
-}
diff --git a/.sqlx/query-cab71411113374c8c388a35281c676b3822629d505e84d51b60162e80a43d190.json b/.sqlx/query-257aa21927a280477b9b59ad726a67a587a0164a38665594d4925b11bf2b260b.json
similarity index 70%
rename from .sqlx/query-cab71411113374c8c388a35281c676b3822629d505e84d51b60162e80a43d190.json
rename to .sqlx/query-257aa21927a280477b9b59ad726a67a587a0164a38665594d4925b11bf2b260b.json
index bd79fb2..36c4b7a 100644
--- a/.sqlx/query-cab71411113374c8c388a35281c676b3822629d505e84d51b60162e80a43d190.json
+++ b/.sqlx/query-257aa21927a280477b9b59ad726a67a587a0164a38665594d4925b11bf2b260b.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "SELECT\n u.id, u.did, u.handle,\n u.email_confirmation_code,\n u.email_confirmation_code_expires_at,\n u.preferred_notification_channel as \"channel: crate::notifications::NotificationChannel\",\n k.key_bytes, k.encryption_version\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.did = $1",
+ "query": "SELECT\n u.id, u.did, u.handle, u.email,\n u.email_confirmation_code,\n u.email_confirmation_code_expires_at,\n u.preferred_notification_channel as \"channel: crate::notifications::NotificationChannel\",\n k.key_bytes, k.encryption_version\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.did = $1",
"describe": {
"columns": [
{
@@ -20,16 +20,21 @@
},
{
"ordinal": 3,
- "name": "email_confirmation_code",
+ "name": "email",
"type_info": "Text"
},
{
"ordinal": 4,
+ "name": "email_confirmation_code",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 5,
"name": "email_confirmation_code_expires_at",
"type_info": "Timestamptz"
},
{
- "ordinal": 5,
+ "ordinal": 6,
"name": "channel: crate::notifications::NotificationChannel",
"type_info": {
"Custom": {
@@ -46,12 +51,12 @@
}
},
{
- "ordinal": 6,
+ "ordinal": 7,
"name": "key_bytes",
"type_info": "Bytea"
},
{
- "ordinal": 7,
+ "ordinal": 8,
"name": "encryption_version",
"type_info": "Int4"
}
@@ -67,10 +72,11 @@
false,
true,
true,
+ true,
false,
false,
true
]
},
- "hash": "cab71411113374c8c388a35281c676b3822629d505e84d51b60162e80a43d190"
+ "hash": "257aa21927a280477b9b59ad726a67a587a0164a38665594d4925b11bf2b260b"
}
diff --git a/.sqlx/query-3727afe601beffcb4551c23eb59fa1c25ca59150c90d4866050b3a073bbe7b4b.json b/.sqlx/query-3727afe601beffcb4551c23eb59fa1c25ca59150c90d4866050b3a073bbe7b4b.json
new file mode 100644
index 0000000..eed231c
--- /dev/null
+++ b/.sqlx/query-3727afe601beffcb4551c23eb59fa1c25ca59150c90d4866050b3a073bbe7b4b.json
@@ -0,0 +1,70 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "SELECT\n handle, email, email_confirmed,\n preferred_notification_channel as \"preferred_channel: crate::notifications::NotificationChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "handle",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 1,
+ "name": "email",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 2,
+ "name": "email_confirmed",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 3,
+ "name": "preferred_channel: crate::notifications::NotificationChannel",
+ "type_info": {
+ "Custom": {
+ "name": "notification_channel",
+ "kind": {
+ "Enum": [
+ "email",
+ "discord",
+ "telegram",
+ "signal"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "ordinal": 4,
+ "name": "discord_verified",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 5,
+ "name": "telegram_verified",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 6,
+ "name": "signal_verified",
+ "type_info": "Bool"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Text"
+ ]
+ },
+ "nullable": [
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "3727afe601beffcb4551c23eb59fa1c25ca59150c90d4866050b3a073bbe7b4b"
+}
diff --git a/.sqlx/query-95165c49f57bb8e130bf1d8444566e1f9521777f994573a4f3cdee809fb63fd7.json b/.sqlx/query-95165c49f57bb8e130bf1d8444566e1f9521777f994573a4f3cdee809fb63fd7.json
new file mode 100644
index 0000000..a461899
--- /dev/null
+++ b/.sqlx/query-95165c49f57bb8e130bf1d8444566e1f9521777f994573a4f3cdee809fb63fd7.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity') RETURNING seq",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "seq",
+ "type_info": "Int8"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Text"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "95165c49f57bb8e130bf1d8444566e1f9521777f994573a4f3cdee809fb63fd7"
+}
diff --git a/.sqlx/query-aadc1f8c79d79e9a32fe6f4bf7e901076532fa2bf8f0b4d0f1bae7aa0f792183.json b/.sqlx/query-f68a05d2c78cc060b43c81b177a24f89c71e7e00dfa5af08ff4584bbe43b4155.json
similarity index 53%
rename from .sqlx/query-aadc1f8c79d79e9a32fe6f4bf7e901076532fa2bf8f0b4d0f1bae7aa0f792183.json
rename to .sqlx/query-f68a05d2c78cc060b43c81b177a24f89c71e7e00dfa5af08ff4584bbe43b4155.json
index 69ba3a5..3dc3c70 100644
--- a/.sqlx/query-aadc1f8c79d79e9a32fe6f4bf7e901076532fa2bf8f0b4d0f1bae7aa0f792183.json
+++ b/.sqlx/query-f68a05d2c78cc060b43c81b177a24f89c71e7e00dfa5af08ff4584bbe43b4155.json
@@ -1,8 +1,14 @@
{
"db_name": "PostgreSQL",
- "query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids)\n VALUES ($1, 'commit', $2, $3, $4, $5, $6)\n ",
+ "query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids)\n VALUES ($1, 'commit', $2, $3, $4, $5, $6)\n RETURNING seq\n ",
"describe": {
- "columns": [],
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "seq",
+ "type_info": "Int8"
+ }
+ ],
"parameters": {
"Left": [
"Text",
@@ -13,7 +19,9 @@
"TextArray"
]
},
- "nullable": []
+ "nullable": [
+ false
+ ]
},
- "hash": "aadc1f8c79d79e9a32fe6f4bf7e901076532fa2bf8f0b4d0f1bae7aa0f792183"
+ "hash": "f68a05d2c78cc060b43c81b177a24f89c71e7e00dfa5af08ff4584bbe43b4155"
}
diff --git a/docs/install-alpine.md b/docs/install-alpine.md
index b85f403..f67ac23 100644
--- a/docs/install-alpine.md
+++ b/docs/install-alpine.md
@@ -137,7 +137,7 @@ echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.profile
```sh
mkdir -p /opt && cd /opt
-git clone https://tangled.org/lewis.moe/bspds.git
+git clone https://tangled.org/lewis.moe/bspds-sandbox bspds
cd bspds
cd frontend
diff --git a/docs/install-containers.md b/docs/install-containers.md
index 46bd50f..74ee45a 100644
--- a/docs/install-containers.md
+++ b/docs/install-containers.md
@@ -92,7 +92,7 @@ cp /opt/bspds/deploy/nginx/nginx-quadlet.conf /srv/bspds/config/nginx.conf
```bash
cd /opt
-git clone https://tangled.org/lewis.moe/bspds.git
+git clone https://tangled.org/lewis.moe/bspds-sandbox bspds
cd bspds
podman build -t bspds:latest .
```
@@ -208,7 +208,7 @@ mkdir -p /srv/bspds/data/{postgres,minio,valkey,certs,acme}
```sh
cd /opt
-git clone https://tangled.org/lewis.moe/bspds.git
+git clone https://tangled.org/lewis.moe/bspds-sandbox bspds
cd bspds
podman build -t bspds:latest .
```
diff --git a/docs/install-debian.md b/docs/install-debian.md
index f359f06..4fe6249 100644
--- a/docs/install-debian.md
+++ b/docs/install-debian.md
@@ -128,7 +128,7 @@ echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
```bash
cd /opt
-git clone https://tangled.org/lewis.moe/bspds.git
+git clone https://tangled.org/lewis.moe/bspds-sandbox bspds
cd bspds
cd frontend
diff --git a/docs/install-openbsd.md b/docs/install-openbsd.md
index 869819f..2053f97 100644
--- a/docs/install-openbsd.md
+++ b/docs/install-openbsd.md
@@ -139,7 +139,7 @@ echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.profile
```sh
mkdir -p /opt && cd /opt
-git clone https://tangled.org/lewis.moe/bspds.git
+git clone https://tangled.org/lewis.moe/bspds-sandbox bspds
cd bspds
cd frontend
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index e038a0d..029d472 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -51,6 +51,8 @@ export interface Session {
handle: string
email?: string
emailConfirmed?: boolean
+ preferredChannel?: string
+ preferredChannelVerified?: boolean
accessJwt: string
refreshJwt: string
}
@@ -95,6 +97,10 @@ export interface ConfirmSignupResult {
refreshJwt: string
handle: string
did: string
+ email?: string
+ emailConfirmed?: boolean
+ preferredChannel?: string
+ preferredChannelVerified?: boolean
}
export const api = {
diff --git a/frontend/src/lib/auth.svelte.ts b/frontend/src/lib/auth.svelte.ts
index 5f8ed54..f7f0db1 100644
--- a/frontend/src/lib/auth.svelte.ts
+++ b/frontend/src/lib/auth.svelte.ts
@@ -108,6 +108,10 @@ export async function confirmSignup(did: string, verificationCode: string): Prom
handle: result.handle,
accessJwt: result.accessJwt,
refreshJwt: result.refreshJwt,
+ email: result.email,
+ emailConfirmed: result.emailConfirmed,
+ preferredChannel: result.preferredChannel,
+ preferredChannelVerified: result.preferredChannelVerified,
}
state.session = session
saveSession(session)
diff --git a/frontend/src/routes/Dashboard.svelte b/frontend/src/routes/Dashboard.svelte
index 5c848f5..e51a8e8 100644
--- a/frontend/src/routes/Dashboard.svelte
+++ b/frontend/src/routes/Dashboard.svelte
@@ -32,7 +32,27 @@
DID
{auth.session.did}
- {#if auth.session.email}
+ {#if auth.session.preferredChannel}
+ Primary Contact
+
+ {#if auth.session.preferredChannel === 'email'}
+ {auth.session.email || 'Email'}
+ {:else if auth.session.preferredChannel === 'discord'}
+ Discord
+ {:else if auth.session.preferredChannel === 'telegram'}
+ Telegram
+ {:else if auth.session.preferredChannel === 'signal'}
+ Signal
+ {:else}
+ {auth.session.preferredChannel}
+ {/if}
+ {#if auth.session.preferredChannelVerified}
+ Verified
+ {:else}
+ Unverified
+ {/if}
+
+ {:else if auth.session.email}
Email
{auth.session.email}
diff --git a/frontend/src/routes/Register.svelte b/frontend/src/routes/Register.svelte
index 640b398..2778b14 100644
--- a/frontend/src/routes/Register.svelte
+++ b/frontend/src/routes/Register.svelte
@@ -205,7 +205,7 @@
disabled={submitting}
required
maxlength="6"
- pattern="[0-9]{6}"
+ inputmode="numeric"
autocomplete="one-time-code"
/>
diff --git a/scripts/install-debian.sh b/scripts/install-debian.sh
new file mode 100755
index 0000000..0272108
--- /dev/null
+++ b/scripts/install-debian.sh
@@ -0,0 +1,645 @@
+#!/bin/bash
+set -euo pipefail
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m'
+
+log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
+log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
+log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
+log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
+
+if [[ $EUID -ne 0 ]]; then
+ log_error "This script must be run as root"
+ exit 1
+fi
+
+if ! grep -qi "debian" /etc/os-release 2>/dev/null; then
+ log_warn "This script is designed for Debian. Proceed with caution on other distros."
+fi
+
+echo -e "${CYAN}"
+echo "╔═══════════════════════════════════════════════════════════════════╗"
+echo "║ BSPDS Installation Script for Debian ║"
+echo "║ AT Protocol Personal Data Server in Rust ║"
+echo "╚═══════════════════════════════════════════════════════════════════╝"
+echo -e "${NC}"
+
+get_public_ips() {
+ IPV4=$(curl -4 -s --max-time 5 ifconfig.me 2>/dev/null || curl -4 -s --max-time 5 icanhazip.com 2>/dev/null || echo "Could not detect")
+ IPV6=$(curl -6 -s --max-time 5 ifconfig.me 2>/dev/null || curl -6 -s --max-time 5 icanhazip.com 2>/dev/null || echo "Not available")
+}
+
+log_info "Detecting public IP addresses..."
+get_public_ips
+
+echo ""
+echo -e "${CYAN}Your server's public IPs:${NC}"
+echo -e " IPv4: ${GREEN}${IPV4}${NC}"
+echo -e " IPv6: ${GREEN}${IPV6}${NC}"
+echo ""
+
+read -p "Enter your PDS domain (e.g., pds.example.com): " PDS_DOMAIN
+if [[ -z "$PDS_DOMAIN" ]]; then
+ log_error "Domain cannot be empty"
+ exit 1
+fi
+
+read -p "Enter your email for Let's Encrypt notifications: " CERTBOT_EMAIL
+if [[ -z "$CERTBOT_EMAIL" ]]; then
+ log_error "Email cannot be empty"
+ exit 1
+fi
+
+echo ""
+echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
+echo -e "${YELLOW}DNS RECORDS REQUIRED${NC}"
+echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
+echo ""
+echo "Before continuing, create these DNS records at your registrar:"
+echo ""
+echo -e "${GREEN}A Record:${NC}"
+echo " Name: ${PDS_DOMAIN}"
+echo " Type: A"
+echo " Value: ${IPV4}"
+echo ""
+if [[ "$IPV6" != "Not available" ]]; then
+echo -e "${GREEN}AAAA Record:${NC}"
+echo " Name: ${PDS_DOMAIN}"
+echo " Type: AAAA"
+echo " Value: ${IPV6}"
+echo ""
+fi
+echo -e "${GREEN}Wildcard A Record (for user handles):${NC}"
+echo " Name: *.${PDS_DOMAIN}"
+echo " Type: A"
+echo " Value: ${IPV4}"
+echo ""
+if [[ "$IPV6" != "Not available" ]]; then
+echo -e "${GREEN}Wildcard AAAA Record (for user handles):${NC}"
+echo " Name: *.${PDS_DOMAIN}"
+echo " Type: AAAA"
+echo " Value: ${IPV6}"
+echo ""
+fi
+echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
+echo ""
+read -p "Have you created these DNS records? (y/N): " DNS_CONFIRMED
+if [[ ! "$DNS_CONFIRMED" =~ ^[Yy]$ ]]; then
+ log_warn "Please create the DNS records and run this script again."
+ exit 0
+fi
+
+CREDENTIALS_FILE="/etc/bspds/.credentials"
+
+if [[ -f "$CREDENTIALS_FILE" ]]; then
+ log_info "Loading existing credentials from previous installation..."
+ source "$CREDENTIALS_FILE"
+ log_success "Credentials loaded"
+else
+ log_info "Generating secure secrets..."
+ JWT_SECRET=$(openssl rand -base64 48)
+ DPOP_SECRET=$(openssl rand -base64 48)
+ MASTER_KEY=$(openssl rand -base64 48)
+ DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
+ MINIO_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
+
+ mkdir -p /etc/bspds
+ cat > "$CREDENTIALS_FILE" << EOF
+JWT_SECRET="$JWT_SECRET"
+DPOP_SECRET="$DPOP_SECRET"
+MASTER_KEY="$MASTER_KEY"
+DB_PASSWORD="$DB_PASSWORD"
+MINIO_PASSWORD="$MINIO_PASSWORD"
+EOF
+ chmod 600 "$CREDENTIALS_FILE"
+ log_success "Secrets generated and saved"
+fi
+
+log_info "Checking swap space..."
+TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
+TOTAL_SWAP_KB=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
+
+if [[ $TOTAL_SWAP_KB -lt 2000000 ]]; then
+ log_info "Adding swap space (needed for compilation)..."
+ if [[ ! -f /swapfile ]]; then
+ SWAP_SIZE="4G"
+ if [[ $TOTAL_MEM_KB -lt 2000000 ]]; then
+ SWAP_SIZE="4G"
+ elif [[ $TOTAL_MEM_KB -lt 4000000 ]]; then
+ SWAP_SIZE="2G"
+ fi
+ fallocate -l $SWAP_SIZE /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=4096
+ chmod 600 /swapfile
+ mkswap /swapfile
+ swapon /swapfile
+ grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
+ log_success "Swap space added ($SWAP_SIZE)"
+ else
+ swapon /swapfile 2>/dev/null || true
+ log_success "Existing swap enabled"
+ fi
+else
+ log_success "Sufficient swap already configured"
+fi
+
+log_info "Updating system packages..."
+apt update && apt upgrade -y
+log_success "System updated"
+
+log_info "Installing build dependencies..."
+apt install -y curl git build-essential pkg-config libssl-dev ca-certificates gnupg lsb-release unzip xxd
+log_success "Build dependencies installed"
+
+log_info "Installing postgres..."
+apt install -y postgresql postgresql-contrib
+systemctl enable postgresql
+systemctl start postgresql
+
+sudo -u postgres psql -c "CREATE USER bspds WITH PASSWORD '${DB_PASSWORD}';" 2>/dev/null || \
+ sudo -u postgres psql -c "ALTER USER bspds WITH PASSWORD '${DB_PASSWORD}';"
+sudo -u postgres psql -c "CREATE DATABASE pds OWNER bspds;" 2>/dev/null || true
+sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO bspds;"
+log_success "postgres installed and configured"
+
+log_info "Installing valkey..."
+apt install -y valkey || {
+ log_warn "valkey not in repos, trying redis..."
+ apt install -y redis-server
+ systemctl enable redis-server
+ systemctl start redis-server
+}
+systemctl enable valkey-server 2>/dev/null || true
+systemctl start valkey-server 2>/dev/null || true
+log_success "valkey/redis installed"
+
+log_info "Installing minio..."
+if [[ ! -f /usr/local/bin/minio ]]; then
+ ARCH=$(dpkg --print-architecture)
+ if [[ "$ARCH" == "amd64" ]]; then
+ curl -fsSL -o /tmp/minio https://dl.min.io/server/minio/release/linux-amd64/minio
+ elif [[ "$ARCH" == "arm64" ]]; then
+ curl -fsSL -o /tmp/minio https://dl.min.io/server/minio/release/linux-arm64/minio
+ else
+ log_error "Unsupported architecture: $ARCH"
+ exit 1
+ fi
+ chmod +x /tmp/minio
+ mv /tmp/minio /usr/local/bin/
+fi
+
+mkdir -p /var/lib/minio/data
+id -u minio-user &>/dev/null || useradd -r -s /sbin/nologin minio-user
+chown -R minio-user:minio-user /var/lib/minio
+
+cat > /etc/default/minio << EOF
+MINIO_ROOT_USER=minioadmin
+MINIO_ROOT_PASSWORD=${MINIO_PASSWORD}
+MINIO_VOLUMES="/var/lib/minio/data"
+MINIO_OPTS="--console-address :9001"
+EOF
+chmod 600 /etc/default/minio
+
+cat > /etc/systemd/system/minio.service << 'EOF'
+[Unit]
+Description=MinIO Object Storage
+After=network.target
+
+[Service]
+User=minio-user
+Group=minio-user
+EnvironmentFile=/etc/default/minio
+ExecStart=/usr/local/bin/minio server $MINIO_VOLUMES $MINIO_OPTS
+Restart=always
+LimitNOFILE=65536
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+systemctl daemon-reload
+systemctl enable minio
+systemctl start minio
+log_success "minio installed"
+
+log_info "Waiting for minio to start..."
+sleep 5
+
+log_info "Installing minio client and creating bucket..."
+if [[ ! -f /usr/local/bin/mc ]]; then
+ ARCH=$(dpkg --print-architecture)
+ if [[ "$ARCH" == "amd64" ]]; then
+ curl -fsSL -o /tmp/mc https://dl.min.io/client/mc/release/linux-amd64/mc
+ elif [[ "$ARCH" == "arm64" ]]; then
+ curl -fsSL -o /tmp/mc https://dl.min.io/client/mc/release/linux-arm64/mc
+ fi
+ chmod +x /tmp/mc
+ mv /tmp/mc /usr/local/bin/
+fi
+
+mc alias remove local 2>/dev/null || true
+mc alias set local http://localhost:9000 minioadmin "${MINIO_PASSWORD}" --api S3v4
+mc mb local/pds-blobs --ignore-existing
+log_success "minio bucket created"
+
+log_info "Installing rust..."
+if [[ -f "$HOME/.cargo/env" ]]; then
+ source "$HOME/.cargo/env"
+fi
+if ! command -v rustc &>/dev/null; then
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
+ source "$HOME/.cargo/env"
+fi
+log_success "rust installed"
+
+log_info "Installing deno..."
+export PATH="$HOME/.deno/bin:$PATH"
+if ! command -v deno &>/dev/null && [[ ! -f "$HOME/.deno/bin/deno" ]]; then
+ curl -fsSL https://deno.land/install.sh | sh
+ grep -q 'deno/bin' ~/.bashrc 2>/dev/null || echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
+fi
+log_success "deno installed"
+
+log_info "Cloning BSPDS..."
+if [[ ! -d /opt/bspds ]]; then
+ git clone https://tangled.org/lewis.moe/bspds-sandbox /opt/bspds
+else
+ log_warn "/opt/bspds already exists, pulling latest..."
+ cd /opt/bspds && git pull
+fi
+cd /opt/bspds
+log_success "BSPDS cloned"
+
+log_info "Building frontend..."
+cd /opt/bspds/frontend
+"$HOME/.deno/bin/deno" task build
+cd /opt/bspds
+log_success "Frontend built"
+
+log_info "Building BSPDS (this may take a while)..."
+source "$HOME/.cargo/env"
+NPROC=$(nproc)
+if [[ $TOTAL_MEM_KB -lt 4000000 ]]; then
+ log_info "Low memory detected, limiting parallel jobs..."
+ CARGO_BUILD_JOBS=1 cargo build --release
+else
+ cargo build --release
+fi
+log_success "BSPDS built"
+
+log_info "Installing sqlx-cli and running migrations..."
+cargo install sqlx-cli --no-default-features --features postgres
+export DATABASE_URL="postgres://bspds:${DB_PASSWORD}@localhost:5432/pds"
+"$HOME/.cargo/bin/sqlx" migrate run
+log_success "Migrations complete"
+
+log_info "Setting up mail trap for testing..."
+mkdir -p /var/spool/bspds-mail
+chown root:root /var/spool/bspds-mail
+chmod 1777 /var/spool/bspds-mail
+
+cat > /usr/local/bin/bspds-sendmail << 'SENDMAIL_EOF'
+#!/bin/bash
+MAIL_DIR="/var/spool/bspds-mail"
+TIMESTAMP=$(date +%Y%m%d-%H%M%S)
+RANDOM_ID=$(head -c 4 /dev/urandom | xxd -p)
+MAIL_FILE="${MAIL_DIR}/${TIMESTAMP}-${RANDOM_ID}.eml"
+
+mkdir -p "$MAIL_DIR"
+
+{
+ echo "X-BSPDS-Received: $(date -Iseconds)"
+ echo "X-BSPDS-Args: $*"
+ echo ""
+ cat
+} > "$MAIL_FILE"
+
+chmod 644 "$MAIL_FILE"
+echo "Mail saved to: $MAIL_FILE" >&2
+exit 0
+SENDMAIL_EOF
+chmod +x /usr/local/bin/bspds-sendmail
+
+cat > /usr/local/bin/bspds-mailq << 'MAILQ_EOF'
+#!/bin/bash
+MAIL_DIR="/var/spool/bspds-mail"
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m'
+
+show_help() {
+ echo "bspds-mailq - View captured emails from BSPDS mail trap"
+ echo ""
+ echo "Usage:"
+ echo " bspds-mailq List all captured emails"
+ echo " bspds-mailq View email by number (from list)"
+ echo " bspds-mailq View email by filename"
+ echo " bspds-mailq latest View the most recent email"
+ echo " bspds-mailq clear Delete all captured emails"
+ echo " bspds-mailq watch Watch for new emails (tail -f style)"
+ echo " bspds-mailq count Show count of emails in queue"
+ echo ""
+}
+
+list_emails() {
+ if [[ ! -d "$MAIL_DIR" ]] || [[ -z "$(ls -A "$MAIL_DIR" 2>/dev/null)" ]]; then
+ echo -e "${YELLOW}No emails in queue.${NC}"
+ return
+ fi
+
+ echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
+ echo -e "${GREEN} BSPDS Mail Queue${NC}"
+ echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
+ echo ""
+
+ local i=1
+ for f in $(ls -t "$MAIL_DIR"/*.eml 2>/dev/null); do
+ local filename=$(basename "$f")
+ local received=$(grep "^X-BSPDS-Received:" "$f" 2>/dev/null | cut -d' ' -f2-)
+ local to=$(grep -i "^To:" "$f" 2>/dev/null | head -1 | cut -d' ' -f2-)
+ local subject=$(grep -i "^Subject:" "$f" 2>/dev/null | head -1 | sed 's/^Subject: *//')
+
+ echo -e "${BLUE}[$i]${NC} ${filename}"
+ echo -e " To: ${GREEN}${to:-unknown}${NC}"
+ echo -e " Subject: ${YELLOW}${subject:-}${NC}"
+ echo -e " Received: ${received:-unknown}"
+ echo ""
+ ((i++))
+ done
+
+ echo -e "${CYAN}Total: $((i-1)) email(s)${NC}"
+}
+
+view_email() {
+ local target="$1"
+ local file=""
+
+ if [[ "$target" == "latest" ]]; then
+ file=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | head -1)
+ elif [[ "$target" =~ ^[0-9]+$ ]]; then
+ file=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | sed -n "${target}p")
+ elif [[ -f "$MAIL_DIR/$target" ]]; then
+ file="$MAIL_DIR/$target"
+ elif [[ -f "$target" ]]; then
+ file="$target"
+ fi
+
+ if [[ -z "$file" ]] || [[ ! -f "$file" ]]; then
+ echo -e "${RED}Email not found: $target${NC}"
+ return 1
+ fi
+
+ echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
+ echo -e "${GREEN} $(basename "$file")${NC}"
+ echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
+ cat "$file"
+ echo ""
+ echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
+}
+
+clear_queue() {
+ local count=$(ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l)
+ if [[ "$count" -eq 0 ]]; then
+ echo -e "${YELLOW}Queue is already empty.${NC}"
+ return
+ fi
+
+ rm -f "$MAIL_DIR"/*.eml
+ echo -e "${GREEN}Cleared $count email(s) from queue.${NC}"
+}
+
+watch_queue() {
+ echo -e "${CYAN}Watching for new emails... (Ctrl+C to stop)${NC}"
+ echo ""
+
+ local last_count=0
+ while true; do
+ local current_count=$(ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l)
+ if [[ "$current_count" -gt "$last_count" ]]; then
+ echo -e "${GREEN}[$(date +%H:%M:%S)] New email received!${NC}"
+ view_email latest
+ last_count=$current_count
+ fi
+ sleep 1
+ done
+}
+
+count_queue() {
+ local count=$(ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l)
+ echo "$count"
+}
+
+case "${1:-}" in
+ ""|list)
+ list_emails
+ ;;
+ latest|[0-9]*)
+ view_email "$1"
+ ;;
+ clear)
+ clear_queue
+ ;;
+ watch)
+ watch_queue
+ ;;
+ count)
+ count_queue
+ ;;
+ help|--help|-h)
+ show_help
+ ;;
+ *)
+ if [[ -f "$MAIL_DIR/$1" ]] || [[ -f "$1" ]]; then
+ view_email "$1"
+ else
+ echo -e "${RED}Unknown command: $1${NC}"
+ show_help
+ exit 1
+ fi
+ ;;
+esac
+MAILQ_EOF
+chmod +x /usr/local/bin/bspds-mailq
+log_success "Mail trap configured"
+
+log_info "Creating BSPDS configuration..."
+mkdir -p /etc/bspds
+
+cat > /etc/bspds/bspds.env << EOF
+SERVER_HOST=127.0.0.1
+SERVER_PORT=3000
+PDS_HOSTNAME=${PDS_DOMAIN}
+
+DATABASE_URL=postgres://bspds:${DB_PASSWORD}@localhost:5432/pds
+DATABASE_MAX_CONNECTIONS=100
+DATABASE_MIN_CONNECTIONS=10
+
+S3_ENDPOINT=http://localhost:9000
+AWS_REGION=us-east-1
+S3_BUCKET=pds-blobs
+AWS_ACCESS_KEY_ID=minioadmin
+AWS_SECRET_ACCESS_KEY=${MINIO_PASSWORD}
+
+VALKEY_URL=redis://localhost:6379
+
+JWT_SECRET=${JWT_SECRET}
+DPOP_SECRET=${DPOP_SECRET}
+MASTER_KEY=${MASTER_KEY}
+
+PLC_DIRECTORY_URL=https://plc.directory
+APPVIEW_URL=https://api.bsky.app
+CRAWLERS=https://bsky.network
+
+AVAILABLE_USER_DOMAINS=${PDS_DOMAIN}
+
+MAIL_FROM_ADDRESS=noreply@${PDS_DOMAIN}
+MAIL_FROM_NAME=BSPDS
+SENDMAIL_PATH=/usr/local/bin/bspds-sendmail
+EOF
+chmod 600 /etc/bspds/bspds.env
+log_success "Configuration created"
+
+log_info "Creating BSPDS service user..."
+id -u bspds &>/dev/null || useradd -r -s /sbin/nologin bspds
+
+cp /opt/bspds/target/release/bspds /usr/local/bin/
+mkdir -p /var/lib/bspds
+cp -r /opt/bspds/frontend/dist /var/lib/bspds/frontend
+chown -R bspds:bspds /var/lib/bspds
+log_success "BSPDS binary installed"
+
+log_info "Creating systemd service..."
+cat > /etc/systemd/system/bspds.service << 'EOF'
+[Unit]
+Description=BSPDS - AT Protocol PDS
+After=network.target postgresql.service minio.service
+
+[Service]
+Type=simple
+User=bspds
+Group=bspds
+EnvironmentFile=/etc/bspds/bspds.env
+Environment=FRONTEND_DIR=/var/lib/bspds/frontend
+ExecStart=/usr/local/bin/bspds
+Restart=always
+RestartSec=5
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+systemctl daemon-reload
+systemctl enable bspds
+systemctl start bspds
+log_success "BSPDS service created and started"
+
+log_info "Installing nginx..."
+apt install -y nginx certbot python3-certbot-nginx
+log_success "nginx installed"
+
+log_info "Configuring nginx..."
+cat > /etc/nginx/sites-available/bspds << EOF
+server {
+ listen 80;
+ listen [::]:80;
+ server_name ${PDS_DOMAIN} *.${PDS_DOMAIN};
+
+ location / {
+ proxy_pass http://127.0.0.1:3000;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade \$http_upgrade;
+ proxy_set_header 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_read_timeout 86400;
+ proxy_send_timeout 86400;
+ client_max_body_size 100M;
+ }
+}
+EOF
+
+ln -sf /etc/nginx/sites-available/bspds /etc/nginx/sites-enabled/
+rm -f /etc/nginx/sites-enabled/default
+nginx -t
+systemctl reload nginx
+log_success "nginx configured"
+
+log_info "Configuring firewall (ufw)..."
+apt install -y ufw
+ufw --force reset
+
+ufw default deny incoming
+ufw default allow outgoing
+
+ufw allow ssh comment 'SSH'
+ufw allow 80/tcp comment 'HTTP'
+ufw allow 443/tcp comment 'HTTPS'
+
+ufw --force enable
+log_success "Firewall configured"
+
+log_info "Obtaining SSL certificate..."
+certbot --nginx -d "${PDS_DOMAIN}" -d "*.${PDS_DOMAIN}" --email "${CERTBOT_EMAIL}" --agree-tos --non-interactive || {
+ log_warn "Wildcard cert failed (requires DNS challenge). Trying single domain..."
+ certbot --nginx -d "${PDS_DOMAIN}" --email "${CERTBOT_EMAIL}" --agree-tos --non-interactive
+}
+log_success "SSL certificate obtained"
+
+log_info "Verifying installation..."
+sleep 3
+if curl -s "http://localhost:3000/xrpc/_health" | grep -q "version"; then
+ log_success "BSPDS is responding!"
+else
+ log_warn "BSPDS may still be starting up. Check: journalctl -u bspds -f"
+fi
+
+echo ""
+echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
+echo -e "${GREEN} INSTALLATION COMPLETE!${NC}"
+echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
+echo ""
+echo -e "Your PDS is now running at: ${GREEN}https://${PDS_DOMAIN}${NC}"
+echo ""
+echo -e "${YELLOW}IMPORTANT: Save these credentials securely!${NC}"
+echo ""
+echo "Database password: ${DB_PASSWORD}"
+echo "MinIO password: ${MINIO_PASSWORD}"
+echo ""
+echo "Configuration file: /etc/bspds/bspds.env"
+echo ""
+echo -e "${CYAN}Useful commands:${NC}"
+echo " journalctl -u bspds -f # View BSPDS logs"
+echo " systemctl status bspds # Check BSPDS status"
+echo " systemctl restart bspds # Restart BSPDS"
+echo " curl https://${PDS_DOMAIN}/xrpc/_health # Health check"
+echo ""
+echo -e "${CYAN}Mail queue (for testing):${NC}"
+echo " bspds-mailq # List all captured emails"
+echo " bspds-mailq latest # View most recent email"
+echo " bspds-mailq 1 # View email #1 from list"
+echo " bspds-mailq watch # Watch for new emails live"
+echo " bspds-mailq clear # Clear all captured emails"
+echo ""
+echo " Emails are saved to: /var/spool/bspds-mail/"
+echo ""
+echo -e "${CYAN}DNS Records Summary:${NC}"
+echo ""
+echo " ${PDS_DOMAIN} A ${IPV4}"
+if [[ "$IPV6" != "Not available" ]]; then
+echo " ${PDS_DOMAIN} AAAA ${IPV6}"
+fi
+echo " *.${PDS_DOMAIN} A ${IPV4}"
+if [[ "$IPV6" != "Not available" ]]; then
+echo " *.${PDS_DOMAIN} AAAA ${IPV6}"
+fi
+echo ""
+echo -e "${GREEN}Enjoy your new AT Protocol PDS!${NC}"
diff --git a/src/api/identity/plc/submit.rs b/src/api/identity/plc/submit.rs
index 6aedc47..ca521bb 100644
--- a/src/api/identity/plc/submit.rs
+++ b/src/api/identity/plc/submit.rs
@@ -218,14 +218,24 @@ pub async fn submit_plc_operation(
}
}
- if let Err(e) = sqlx::query!(
- "INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity')",
+ match sqlx::query!(
+ "INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity') RETURNING seq",
did
)
- .execute(&state.db)
+ .fetch_one(&state.db)
.await
{
- warn!("Failed to sequence identity event: {:?}", e);
+ Ok(row) => {
+ if let Err(e) = sqlx::query(&format!("NOTIFY repo_updates, '{}'", row.seq))
+ .execute(&state.db)
+ .await
+ {
+ warn!("Failed to notify identity event: {:?}", e);
+ }
+ }
+ Err(e) => {
+ warn!("Failed to sequence identity event: {:?}", e);
+ }
}
info!("Submitted PLC operation for user {}", did);
diff --git a/src/api/repo/import.rs b/src/api/repo/import.rs
index 8b6f6b2..efa8ab8 100644
--- a/src/api/repo/import.rs
+++ b/src/api/repo/import.rs
@@ -390,10 +390,11 @@ async fn sequence_import_event(
let blobs: Vec = vec![];
let blocks_cids: Vec = vec![];
- sqlx::query!(
+ let seq_row = sqlx::query!(
r#"
INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids)
VALUES ($1, 'commit', $2, $3, $4, $5, $6)
+ RETURNING seq
"#,
did,
commit_cid,
@@ -402,8 +403,12 @@ async fn sequence_import_event(
&blobs,
&blocks_cids
)
- .execute(&state.db)
+ .fetch_one(&state.db)
.await?;
+ sqlx::query(&format!("NOTIFY repo_updates, '{}'", seq_row.seq))
+ .execute(&state.db)
+ .await?;
+
Ok(())
}
diff --git a/src/api/server/session.rs b/src/api/server/session.rs
index 55eb640..426716b 100644
--- a/src/api/server/session.rs
+++ b/src/api/server/session.rs
@@ -176,16 +176,34 @@ pub async fn get_session(
State(state): State,
BearerAuth(auth_user): BearerAuth,
) -> Response {
- match sqlx::query!("SELECT handle, email FROM users WHERE did = $1", auth_user.did)
- .fetch_optional(&state.db)
- .await
+ match sqlx::query!(
+ r#"SELECT
+ handle, email, email_confirmed,
+ preferred_notification_channel as "preferred_channel: crate::notifications::NotificationChannel",
+ discord_verified, telegram_verified, signal_verified
+ FROM users WHERE did = $1"#,
+ auth_user.did
+ )
+ .fetch_optional(&state.db)
+ .await
{
- Ok(Some(row)) => Json(json!({
- "handle": row.handle,
- "did": auth_user.did,
- "email": row.email,
- "didDoc": {}
- })).into_response(),
+ Ok(Some(row)) => {
+ let (preferred_channel, preferred_channel_verified) = match row.preferred_channel {
+ crate::notifications::NotificationChannel::Email => ("email", row.email_confirmed),
+ crate::notifications::NotificationChannel::Discord => ("discord", row.discord_verified),
+ crate::notifications::NotificationChannel::Telegram => ("telegram", row.telegram_verified),
+ crate::notifications::NotificationChannel::Signal => ("signal", row.signal_verified),
+ };
+ Json(json!({
+ "handle": row.handle,
+ "did": auth_user.did,
+ "email": row.email,
+ "emailConfirmed": row.email_confirmed,
+ "preferredChannel": preferred_channel,
+ "preferredChannelVerified": preferred_channel_verified,
+ "didDoc": {}
+ })).into_response()
+ }
Ok(None) => ApiError::AuthenticationFailed.into_response(),
Err(e) => {
error!("Database error in get_session: {:?}", e);
@@ -373,16 +391,35 @@ pub async fn refresh_session(
return ApiError::InternalError.into_response();
}
- match sqlx::query!("SELECT handle FROM users WHERE did = $1", session_row.did)
- .fetch_optional(&state.db)
- .await
+ match sqlx::query!(
+ r#"SELECT
+ handle, email, email_confirmed,
+ preferred_notification_channel as "preferred_channel: crate::notifications::NotificationChannel",
+ discord_verified, telegram_verified, signal_verified
+ FROM users WHERE did = $1"#,
+ session_row.did
+ )
+ .fetch_optional(&state.db)
+ .await
{
- Ok(Some(u)) => Json(json!({
- "accessJwt": new_access_meta.token,
- "refreshJwt": new_refresh_meta.token,
- "handle": u.handle,
- "did": session_row.did
- })).into_response(),
+ Ok(Some(u)) => {
+ let (preferred_channel, preferred_channel_verified) = match u.preferred_channel {
+ crate::notifications::NotificationChannel::Email => ("email", u.email_confirmed),
+ crate::notifications::NotificationChannel::Discord => ("discord", u.discord_verified),
+ crate::notifications::NotificationChannel::Telegram => ("telegram", u.telegram_verified),
+ crate::notifications::NotificationChannel::Signal => ("signal", u.signal_verified),
+ };
+ Json(json!({
+ "accessJwt": new_access_meta.token,
+ "refreshJwt": new_refresh_meta.token,
+ "handle": u.handle,
+ "did": session_row.did,
+ "email": u.email,
+ "emailConfirmed": u.email_confirmed,
+ "preferredChannel": preferred_channel,
+ "preferredChannelVerified": preferred_channel_verified
+ })).into_response()
+ }
Ok(None) => {
error!("User not found for existing session: {}", session_row.did);
ApiError::InternalError.into_response()
@@ -408,6 +445,10 @@ pub struct ConfirmSignupOutput {
pub refresh_jwt: String,
pub handle: String,
pub did: String,
+ pub email: Option,
+ pub email_confirmed: bool,
+ pub preferred_channel: String,
+ pub preferred_channel_verified: bool,
}
pub async fn confirm_signup(
@@ -418,7 +459,7 @@ pub async fn confirm_signup(
let row = match sqlx::query!(
r#"SELECT
- u.id, u.did, u.handle,
+ u.id, u.did, u.handle, u.email,
u.email_confirmation_code,
u.email_confirmation_code_expires_at,
u.preferred_notification_channel as "channel: crate::notifications::NotificationChannel",
@@ -527,11 +568,23 @@ pub async fn confirm_signup(
warn!("Failed to enqueue welcome notification: {:?}", e);
}
+ let email_confirmed = matches!(row.channel, crate::notifications::NotificationChannel::Email);
+ let preferred_channel = match row.channel {
+ crate::notifications::NotificationChannel::Email => "email",
+ crate::notifications::NotificationChannel::Discord => "discord",
+ crate::notifications::NotificationChannel::Telegram => "telegram",
+ crate::notifications::NotificationChannel::Signal => "signal",
+ };
+
Json(ConfirmSignupOutput {
access_jwt: access_meta.token,
refresh_jwt: refresh_meta.token,
handle: row.handle,
did: row.did,
+ email: row.email,
+ email_confirmed,
+ preferred_channel: preferred_channel.to_string(),
+ preferred_channel_verified: true,
}).into_response()
}
diff --git a/src/sync/listener.rs b/src/sync/listener.rs
index 016b6e1..4e35f78 100644
--- a/src/sync/listener.rs
+++ b/src/sync/listener.rs
@@ -57,6 +57,7 @@ async fn listen_loop(state: AppState) -> anyhow::Result<()> {
loop {
let notification = listener.recv().await?;
let payload = notification.payload();
+ debug!(payload = %payload, "Received postgres notification");
let seq_id: i64 = match payload.parse() {
Ok(id) => id,
@@ -110,7 +111,14 @@ async fn listen_loop(state: AppState) -> anyhow::Result<()> {
.await?;
if let Some(event) = event {
- let _ = state.firehose_tx.send(event);
+ match state.firehose_tx.send(event) {
+ Ok(receiver_count) => {
+ debug!(seq = seq_id, receivers = receiver_count, "Broadcast event to firehose");
+ }
+ Err(e) => {
+ warn!(seq = seq_id, error = %e, "Failed to broadcast event (no receivers?)");
+ }
+ }
LAST_BROADCAST_SEQ.store(seq_id, Ordering::SeqCst);
} else {
warn!(seq = seq_id, "Received notification but could not find row in repo_seq");