diff --git a/.env.hold.example b/.env.hold.example
index a282e79..a490c98 100644
--- a/.env.hold.example
+++ b/.env.hold.example
@@ -29,8 +29,8 @@ AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
# S3 Region
-# Examples: us-east-1, us-west-2, eu-west-1
-# For UpCloud: us-chi1, us-nyc1, de-fra1, uk-lon1, sg-sin1
+# For third-party S3 providers, this is ignored when S3_ENDPOINT is set,
+# but must be a valid AWS region (e.g., us-east-1) to pass validation.
# Default: us-east-1
AWS_REGION=us-east-1
@@ -61,6 +61,11 @@ S3_BUCKET=atcr-blobs
# Default: false
HOLD_PUBLIC=false
+# ATProto relay endpoint for requesting crawl on startup
+# This makes the hold's embedded PDS discoverable by the relay network
+# Default: https://bsky.network (set to empty string to disable)
+# HOLD_RELAY_ENDPOINT=https://bsky.network
+
# ==============================================================================
# Embedded PDS Configuration
# ==============================================================================
diff --git a/.tangled/workflows/tests.yml b/.tangled/workflows/tests.yml
index 30f559e..a17423f 100644
--- a/.tangled/workflows/tests.yml
+++ b/.tangled/workflows/tests.yml
@@ -6,7 +6,7 @@ when:
engine: kubernetes
image: golang:1.25-trixie
-architecture: arm64
+architecture: amd64
steps:
- name: Download and Generate
diff --git a/Makefile b/Makefile
index 33f06c3..2767c66 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,8 @@
# Build targets for the ATProto Container Registry
.PHONY: all build build-appview build-hold build-credential-helper build-oauth-helper \
- generate test test-race test-verbose lint clean help install-credential-helper
+ generate test test-race test-verbose lint clean help install-credential-helper \
+ develop develop-detached develop-down
.DEFAULT_GOAL := help
@@ -80,6 +81,27 @@ install-credential-helper: build-credential-helper ## Install credential helper
install -m 755 bin/docker-credential-atcr /usr/local/sbin/docker-credential-atcr
@echo "✓ Installed docker-credential-atcr to /usr/local/sbin/"
+##@ Docker Targets
+
+develop: ## Build Docker images and start docker-compose for development
+ @echo "→ Building Docker images..."
+ docker-compose build
+ @echo "→ Starting docker-compose..."
+ docker-compose up
+
+develop-detached: ## Build and start docker-compose in detached mode
+ @echo "→ Building Docker images..."
+ docker-compose build
+ @echo "→ Starting docker-compose (detached)..."
+ docker-compose up -d
+ @echo "✓ Services started in background"
+ @echo " AppView: http://localhost:5000"
+ @echo " Hold: http://localhost:8080"
+
+develop-down: ## Stop docker-compose services
+ @echo "→ Stopping docker-compose..."
+ docker-compose down
+
##@ Utility Targets
clean: ## Remove built binaries and generated assets
diff --git a/cmd/hold/main.go b/cmd/hold/main.go
index 3fbeff7..1d14030 100644
--- a/cmd/hold/main.go
+++ b/cmd/hold/main.go
@@ -179,6 +179,16 @@ func main() {
}
}
+ // Request crawl from relay to make PDS discoverable
+ if cfg.Server.RelayEndpoint != "" {
+ slog.Info("Requesting crawl from relay", "relay", cfg.Server.RelayEndpoint)
+ if err := hold.RequestCrawl(cfg.Server.RelayEndpoint, cfg.Server.PublicURL); err != nil {
+ slog.Warn("Failed to request crawl from relay", "error", err)
+ } else {
+ slog.Info("Crawl requested successfully")
+ }
+ }
+
// Wait for signal or server error
select {
case err := <-serverErr:
diff --git a/deploy/.env.prod.template b/deploy/.env.prod.template
index 9873160..b079216 100644
--- a/deploy/.env.prod.template
+++ b/deploy/.env.prod.template
@@ -115,10 +115,10 @@ AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
# S3 Region (for distribution S3 driver)
-# UpCloud regions: us-chi1, us-nyc1, de-fra1, uk-lon1, sg-sin1, etc.
-# Note: Use AWS_REGION (not S3_REGION) - this is what the hold service expects
+# For third-party S3 providers (UpCloud, Storj, Minio), this value is ignored
+# when S3_ENDPOINT is set, but must be a valid AWS region to pass validation.
# Default: us-east-1
-AWS_REGION=us-chi1
+AWS_REGION=us-east-1
# S3 Bucket Name
# Create this bucket in UpCloud Object Storage
@@ -134,11 +134,6 @@ S3_BUCKET=atcr
# Custom domains break presigned URL generation
S3_ENDPOINT=https://6vmss.upcloudobjects.com
-# S3 Region Endpoint (alternative to S3_ENDPOINT)
-# Use this if your S3 driver requires region-specific endpoint format
-# Example: s3.us-chi1.upcloudobjects.com
-# S3_REGION_ENDPOINT=
-
# ==============================================================================
# AppView Configuration
# ==============================================================================
@@ -231,13 +226,12 @@ ATCR_BACKFILL_INTERVAL=1h
# ☐ Set HOLD_OWNER (your ATProto DID)
# ☐ Set HOLD_DATABASE_DIR (default: /var/lib/atcr-hold) - enables embedded PDS
# ☐ Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
-# ☐ Set AWS_REGION (e.g., us-chi1)
# ☐ Set S3_BUCKET (created in UpCloud Object Storage)
-# ☐ Set S3_ENDPOINT (UpCloud endpoint or custom domain)
+# ☐ Set S3_ENDPOINT (UpCloud bucket endpoint, e.g., https://6vmss.upcloudobjects.com)
# ☐ Configured DNS records:
# - A record: atcr.io → server IP
# - A record: hold01.atcr.io → server IP
-# - CNAME: blobs.atcr.io → [bucket].us-chi1.upcloudobjects.com
+# - CNAME: blobs.atcr.io → [bucket].upcloudobjects.com
# ☐ Disabled Cloudflare proxy (gray cloud, not orange)
# ☐ Waited for DNS propagation (check with: dig atcr.io)
#
diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml
index 18a720f..21d6e5a 100644
--- a/deploy/docker-compose.prod.yml
+++ b/deploy/docker-compose.prod.yml
@@ -109,10 +109,9 @@ services:
# S3/UpCloud Object Storage configuration
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-}
- AWS_REGION: ${AWS_REGION:-us-chi1}
+ AWS_REGION: ${AWS_REGION:-us-east-1}
S3_BUCKET: ${S3_BUCKET:-atcr-blobs}
S3_ENDPOINT: ${S3_ENDPOINT:-}
- S3_REGION_ENDPOINT: ${S3_REGION_ENDPOINT:-}
# Logging
ATCR_LOG_LEVEL: ${ATCR_LOG_LEVEL:-debug}
@@ -160,8 +159,6 @@ configs:
# Preserve original host header
header_up Host {host}
header_up X-Real-IP {remote_host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
}
# Enable compression
@@ -183,8 +180,6 @@ configs:
# Preserve original host header
header_up Host {host}
header_up X-Real-IP {remote_host}
- header_up X-Forwarded-For {remote_host}
- header_up X-Forwarded-Proto {scheme}
}
# Enable compression
diff --git a/pkg/appview/jetstream/processor_test.go b/pkg/appview/jetstream/processor_test.go
index 9132227..c54237d 100644
--- a/pkg/appview/jetstream/processor_test.go
+++ b/pkg/appview/jetstream/processor_test.go
@@ -70,6 +70,7 @@ func setupTestDB(t *testing.T) *sql.DB {
platform_os TEXT,
platform_variant TEXT,
platform_os_version TEXT,
+ is_attestation BOOLEAN DEFAULT FALSE,
reference_index INTEGER NOT NULL,
PRIMARY KEY(manifest_id, reference_index)
);
diff --git a/pkg/appview/templates/components/head.html b/pkg/appview/templates/components/head.html
index 5957327..86ed9b2 100644
--- a/pkg/appview/templates/components/head.html
+++ b/pkg/appview/templates/components/head.html
@@ -2,6 +2,45 @@
+
+ {{ if .ViewedUser }}
+
+
+
+ {{ if .ViewedUser.Avatar }}
+
+ {{ else }}
+
+ {{ end }}
+
+
+ {{ else if .Repository }}
+
+
+ {{ if .Repository.Description }}
+
+ {{ else }}
+
+ {{ end }}
+ {{ if .Repository.IconURL }}
+
+ {{ else if .Owner.Avatar }}
+
+ {{ else }}
+
+ {{ end }}
+
+
+ {{ else }}
+
+
+
+
+
+
+ {{ end }}
+
+
diff --git a/pkg/hold/config.go b/pkg/hold/config.go
index 50ee770..05693ff 100644
--- a/pkg/hold/config.go
+++ b/pkg/hold/config.go
@@ -6,7 +6,11 @@
package hold
import (
+ "bytes"
+ "encoding/json"
"fmt"
+ "net/http"
+ "net/url"
"os"
"path/filepath"
"time"
@@ -67,6 +71,10 @@ type ServerConfig struct {
// DisablePresignedURLs forces proxy mode even with S3 configured (for testing) (from env: DISABLE_PRESIGNED_URLS)
DisablePresignedURLs bool `yaml:"disable_presigned_urls"`
+ // RelayEndpoint is the ATProto relay URL to request crawl from on startup (from env: HOLD_RELAY_ENDPOINT)
+ // If empty, no crawl request is made. Default: https://bsky.network
+ RelayEndpoint string `yaml:"relay_endpoint"`
+
// ReadTimeout for HTTP requests
ReadTimeout time.Duration `yaml:"read_timeout"`
@@ -103,6 +111,7 @@ func LoadConfigFromEnv() (*Config, error) {
cfg.Server.Public = os.Getenv("HOLD_PUBLIC") == "true"
cfg.Server.TestMode = os.Getenv("TEST_MODE") == "true"
cfg.Server.DisablePresignedURLs = os.Getenv("DISABLE_PRESIGNED_URLS") == "true"
+ cfg.Server.RelayEndpoint = getEnvOrDefault("HOLD_RELAY_ENDPOINT", "https://bsky.network")
cfg.Server.ReadTimeout = 5 * time.Minute // Increased for large blob uploads
cfg.Server.WriteTimeout = 5 * time.Minute // Increased for large blob uploads
@@ -180,3 +189,48 @@ func getEnvOrDefault(key, defaultValue string) string {
}
return defaultValue
}
+
+// RequestCrawl sends a crawl request to the ATProto relay for the given hostname.
+// This makes the hold's PDS discoverable by the relay network.
+func RequestCrawl(relayEndpoint, publicURL string) error {
+ if relayEndpoint == "" {
+ return nil // No relay configured, skip
+ }
+
+ // Extract hostname from public URL
+ parsed, err := url.Parse(publicURL)
+ if err != nil {
+ return fmt.Errorf("failed to parse public URL: %w", err)
+ }
+ hostname := parsed.Host
+
+ // Build the request URL
+ requestURL := relayEndpoint + "/xrpc/com.atproto.sync.requestCrawl"
+
+ // Create request body
+ body := map[string]string{"hostname": hostname}
+ bodyJSON, err := json.Marshal(body)
+ if err != nil {
+ return fmt.Errorf("failed to marshal request body: %w", err)
+ }
+
+ // Make the request
+ client := &http.Client{Timeout: 10 * time.Second}
+ req, err := http.NewRequest("POST", requestURL, bytes.NewReader(bodyJSON))
+ if err != nil {
+ return fmt.Errorf("failed to create request: %w", err)
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to send request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return fmt.Errorf("relay returned status %d", resp.StatusCode)
+ }
+
+ return nil
+}