Replace PostgreSQL and Docker deployment with embedded Turso, Drizzle relational queries v2, native systemd deployment on port 43821, and fresh SQLite migrations.

Hop-State: A_06FP5KEDBTB4A9ZT7QB498G
Hop-Proposal: R_06FP5KDWMCKVVMQZT4MVZ28
Hop-Task: T_06FP5DZ7T0G45FG93PT90B8
Hop-Attempt: AT_06FP5DZ7T0PKQW99V27JCV8
This commit is contained in:
2026-07-14 15:44:42 -07:00
committed by Hop
184 changed files with 8375 additions and 53944 deletions
-18
View File
@@ -1,18 +0,0 @@
.git
.github
.next
.vercel
node_modules
coverage
site-work
*.log
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
+7 -3
View File
@@ -2,7 +2,11 @@
# Copy to .env and adjust values for your machine. # Copy to .env and adjust values for your machine.
# Required: local database # Required: local database
DATABASE_URL=postgres://postgres:postgres@localhost:5432/synapsis # Embedded Turso database file. Relative paths resolve from the app directory.
DATABASE_PATH=./data/synapsis.db
# Synapsis' standard local bind port. Override it if this port is already in use.
PORT=43821
# Required: used for sessions, node key encryption, and bot API key encryption # Required: used for sessions, node key encryption, and bot API key encryption
AUTH_SECRET=replace-with-a-long-random-secret AUTH_SECRET=replace-with-a-long-random-secret
@@ -11,8 +15,8 @@ AUTH_SECRET=replace-with-a-long-random-secret
ADMIN_EMAILS=admin@example.com ADMIN_EMAILS=admin@example.com
# Recommended: local node identity # Recommended: local node identity
NEXT_PUBLIC_NODE_DOMAIN=localhost:3000 NEXT_PUBLIC_NODE_DOMAIN=localhost:43821
NEXT_PUBLIC_APP_URL=http://localhost:3000 NEXT_PUBLIC_APP_URL=http://localhost:43821
# Optional: node metadata shown before a node record exists in the database # Optional: node metadata shown before a node record exists in the database
# NEXT_PUBLIC_NODE_NAME=Synapsis Local # NEXT_PUBLIC_NODE_NAME=Synapsis Local
+7 -16
View File
@@ -36,21 +36,12 @@ jobs:
src/lib/bots/mentionHandler.property.test.ts src/lib/bots/mentionHandler.property.test.ts
src/lib/bots/scheduler.test.ts src/lib/bots/scheduler.test.ts
- name: Embedded database smoke test
run: DATABASE_PATH=/tmp/synapsis-ci.db npm run db:migrate
- name: Production build - name: Production build
run: npm run build run: npm run build
env:
docker-validation: AUTH_SECRET: ci-test-secret
runs-on: ubuntu-latest DATABASE_PATH: /tmp/synapsis-ci.db
NEXT_PUBLIC_NODE_DOMAIN: localhost:43821
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate production compose config
run: docker compose --env-file docker/.env.example config > /tmp/synapsis-compose.txt
- name: Validate source-build compose config
run: docker compose -f docker/docker-compose.yml --env-file docker/.env.example config > /tmp/synapsis-build-compose.txt
- name: Build application image
run: docker build -f docker/Dockerfile .
-62
View File
@@ -1,62 +0,0 @@
name: Build and Push Docker Image to GHCR
on:
workflow_dispatch:
inputs:
tag:
description: 'Optional explicit version tag (for example 2026.03.09.10). Leave blank to auto-increment.'
required: false
default: ''
# Also build on version tags
push:
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: gnosyslabs/synapsis
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
env:
IMAGE_REPO: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
PLATFORMS: linux/amd64,linux/arm64
PRUNE_BUILD_CACHE: '0'
APP_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || '' }}
EXTRA_TAGS: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '' }}
run: |
if [ -n "$EXTRA_TAGS" ]; then
EXTRA_TAGS="${EXTRA_TAGS#v}"
export EXTRA_TAGS
if [ -z "$APP_VERSION" ]; then
APP_VERSION="$EXTRA_TAGS"
export APP_VERSION
fi
fi
chmod +x ./scripts/docker-publish.sh
./scripts/docker-publish.sh
+6
View File
@@ -47,6 +47,12 @@ next-env.d.ts
# uploads # uploads
/public/uploads/ /public/uploads/
# embedded Turso / SQLite data
/data/
*.db
*.db-shm
*.db-wal
# IDE # IDE
.vscode/ .vscode/
.kiro/ .kiro/
-247
View File
@@ -1,247 +0,0 @@
# Quick Start with Docker
Get your Synapsis node running in under 10 minutes with Docker. This is the **recommended and easiest** way to self-host.
## Prerequisites
- A Linux server with a domain pointed to it
- A domain name pointed to your server
## Quick Start (5 Minutes)
```bash
# 1. Bootstrap the deployment directory
curl -fsSL https://synapsis.social/install.sh | bash
# 2. Review the generated config
nano /opt/synapsis/.env
# 3. Start your node
cd /opt/synapsis
docker compose up -d
```
Done! Your node is live at `https://your-domain.com` with automatic SSL.
## Existing nginx / reverse proxy host
If your server already has nginx, Traefik, or another reverse proxy using `80/443`, use proxyless mode instead:
```bash
curl -fsSL https://synapsis.social/install.sh | PROXY=none bash
nano /opt/synapsis/.env
cd /opt/synapsis
docker compose up -d
```
In `PROXY=none` mode:
- Synapsis skips the bundled Caddy service
- the app binds to `127.0.0.1:${PORT}`
- your existing reverse proxy should forward traffic to that localhost port
Example nginx upstream:
```nginx
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
```
## Detailed Setup
### 1. Bootstrap the Deployment Directory
```bash
curl -fsSL https://synapsis.social/install.sh | bash
```
If Docker is missing, the installer will install it for you on supported Linux hosts.
This downloads:
- `docker-compose.yml`
- `Caddyfile`
- `caddy-entrypoint.sh`
- `.env.example`
- `.env`
The installer also generates `AUTH_SECRET` and `DB_PASSWORD` if `openssl` is available.
### 2. Configure Environment
Edit `/opt/synapsis/.env` with your settings:
```bash
nano /opt/synapsis/.env
```
**Required variables:**
```env
# Your domain
DOMAIN=your-domain.com
# Admin email(s)
ADMIN_EMAILS=you@example.com
# Generate with: openssl rand -hex 32
AUTH_SECRET=your-secret-key-here
# Database password
DB_PASSWORD=your-secure-password
```
You can also set these before running the installer to prefill `.env`:
```bash
DOMAIN=your-domain.com
ADMIN_EMAILS=you@example.com
```
Thats it. Save and exit.
### 3. Start Synapsis
```bash
cd /opt/synapsis
docker compose up -d
```
This starts:
- **PostgreSQL** database
- **Synapsis** app (pre-built image from GitHub Container Registry)
- **Caddy** reverse proxy with automatic SSL
Visit `https://your-domain.com` to create your admin account.
## Storage Model
### Node installation
The node itself only needs Docker, PostgreSQL, and a domain to start.
### User media storage
At the moment, new user registration requires each account to provide **its own S3-compatible storage credentials** for media uploads. That requirement is enforced in the app today, so plan onboarding accordingly.
## Configuration Reference
### Core Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `DOMAIN` | Yes | Your domain name |
| `AUTH_SECRET` | Yes | Generate with `openssl rand -hex 32` |
| `ADMIN_EMAILS` | Yes | Comma-separated admin emails |
| `DB_PASSWORD` | Yes | Database password |
### Optional Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `BOT_MAX_PER_USER` | 5 | Max AI bots per user |
| `PORT` | auto | Application port. Default installs use `auto` to scan 3000-3020. In `PROXY=none` mode this is the localhost port your reverse proxy should target. |
| `NEXT_PUBLIC_NODE_DOMAIN` | `DOMAIN` | Override node domain if needed |
| `NEXT_PUBLIC_APP_URL` | Derived from domain | Public URL used by background jobs |
## Updating Synapsis
Updating is a single command:
```bash
cd /opt/synapsis
docker compose pull
docker compose up -d
```
This will:
- Pull the latest pre-built image from GitHub Container Registry
- Recreate containers if needed
- Run database migrations automatically
- Keep your data volumes intact
### Check Update Status
```bash
# See running containers
docker compose ps
# View logs
docker compose logs -f app
```
## Troubleshooting
### Container won't start
```bash
# Check logs
docker compose logs app
# Common issues:
# - Missing required env vars
# - Port 3000 already in use
```
### Database connection errors
```bash
# Check database container
docker compose logs postgres
# Verify env vars are loaded
docker compose exec app env | grep DATABASE
```
### Port already in use
If `80` or `443` is already in use, your server already has another reverse proxy bound there. Use `PROXY=none` instead of the default Caddy install:
```bash
curl -fsSL https://synapsis.social/install.sh | PROXY=none bash
```
For the application port itself, Synapsis uses `PORT=auto` by default, which automatically finds an available port between 3000-3020. If you need to use a specific port:
```bash
# Edit .env and set a specific port
PORT=3001
# Restart
docker compose down
docker compose up -d
```
### Reset everything (data loss!)
```bash
docker compose down -v # Remove containers AND volumes
docker compose up -d # Start fresh
```
### View all logs
```bash
# All services
docker compose logs -f
# Just the app
docker compose logs -f app
# Just the database
docker compose logs -f postgres
```
## Next Steps
- [Configure your node settings](/user-guide)
- [Learn about the Swarm](/swarm)
- [Learn how Synapsis differs from traditional federation](/user-guide/synapsis-difference)
-72
View File
@@ -1,72 +0,0 @@
# Synapsis Docker Image
# Multi-stage build for production
# Stage 1: Dependencies
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Copy package files
COPY package.json package-lock.json* ./
RUN npm ci --only=production
# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
# Copy deps from previous stage
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build the application
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN npm run build
# Stage 3: Runner
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Install tools needed for port detection and healthchecks
RUN apk add --no-cache netcat-openbsd wget
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy necessary files
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./
COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle
COPY --from=builder --chown=nextjs:nodejs /app/drizzle.config.ts ./
COPY --from=builder --chown=nextjs:nodejs /app/src/db/schema.ts ./src/db/schema.ts
# Copy and set up entrypoint script
COPY --chown=nextjs:nodejs scripts/docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Create shared directory for port file (must be writable by nextjs)
RUN mkdir -p /var/run/synapsis && chown nextjs:nodejs /var/run/synapsis
# Switch to non-root user
USER nextjs
# Expose port range for auto-detection
EXPOSE 3000-3020
# Set environment variables (can be overridden at runtime)
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Health check (reads dynamic port file when PORT=auto)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD sh -c 'PORT_FILE=/var/run/synapsis/port; PORT=3000; if [ -f "$PORT_FILE" ]; then PORT=$(cat "$PORT_FILE"); fi; case "$PORT" in ""|*[!0-9]*) PORT=3000;; esac; wget -q --spider "http://127.0.0.1:${PORT}/api/health" || exit 1'
# Use entrypoint script for port detection
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
+14 -135
View File
@@ -1,159 +1,38 @@
# How To Work On Synapsis # Working on Synapsis
This is the practical workflow for developing Synapsis without turning every bugfix into a full Docker release.
## Basic Rule Synapsis uses an embedded Turso database and runs as a native Node.js process. Docker and a local PostgreSQL server are not part of the development or deployment workflow.
Use three different loops for three different jobs:
1. `npm run dev` for normal feature work and bugfixes
2. local Docker source builds when you need container parity
3. GHCR publish only when you actually want the server to update
Do **not** rebuild and push a Docker image for every tiny fix. Batch fixes together, verify them locally, then publish when the server needs the new version. ## Local loop
## 1. Normal Local Development
Use this for most day-to-day work.
```bash ```bash
npm install npm install
cp .env.example .env cp .env.example .env
npm run db:push npm run db:migrate
npm run dev npm run dev
``` ```
Useful verification commands: Before handing off a change:
```bash ```bash
npm run type-check npm run type-check
npm run build
npm test npm test
npm run build
``` ```
Use this loop when: When the schema changes, edit `src/db/schema.ts`, generate a migration with `npm run db:generate`, and verify it against a fresh database:
- changing UI
- fixing API logic
- working on auth, feed logic, posting, bots, or settings
- you do not specifically need to test the Docker runtime
## 2. Local Docker Parity Test
Use this when you want to know whether the app still works inside the actual container setup. This compose file builds from your local source tree:
```bash ```bash
cd docker DATABASE_PATH=/tmp/synapsis-test.db npm run db:migrate
cp .env.example .env
docker compose up --build
``` ```
That uses [docker/docker-compose.yml](/Users/christopher/Dev/Synapsis/Synapsis/docker/docker-compose.yml) and [docker/Dockerfile](/Users/christopher/Dev/Synapsis/Synapsis/docker/Dockerfile). ## VPS workflow
Use this loop when: Production runs through `synapsis.service`, listening on `127.0.0.1:43821`. Reverse-proxy configuration belongs to the host operator.
- Docker-specific startup behavior matters
- you changed the Dockerfile or entrypoint
- you changed env handling, healthchecks, migrations, ports, or install flow
## 3. Production Image Publish
Use this only when you want the server or end users to pull a new image. The production install uses [docker-compose.yml](/Users/christopher/Dev/Synapsis/Synapsis/docker-compose.yml) and `ghcr.io/gnosyslabs/synapsis:latest`.
### First-time GHCR auth on this machine
```bash
gh auth refresh -h github.com -s read:packages -s write:packages
gh auth token | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
```
### Publish the image
Push code first:
```bash ```bash
git push origin main sudo /opt/synapsis/deploy/update.sh
sudo systemctl status synapsis
sudo journalctl -u synapsis -f
``` ```
Then build and push the multi-arch image: The updater makes a timestamped database backup, pulls with `--ff-only`, installs dependencies, runs migrations, builds, and restarts the service.
```bash
BUILDER=colima ./scripts/docker-publish.sh
```
That publishes:
- `ghcr.io/gnosyslabs/synapsis:latest`
- `ghcr.io/gnosyslabs/synapsis:<YYYY.MM.DD.N>`
- `ghcr.io/gnosyslabs/synapsis:<short-sha>`
If you are not on a Mac/Colima setup, set `BUILDER` to your buildx builder or leave it empty to use the default builder.
To force an explicit version instead of auto-incrementing:
```bash
APP_VERSION=2026.03.09.10 BUILDER=colima ./scripts/docker-publish.sh
```
## 4. Update The Server
Once a new image is published, update the server with:
```bash
cd /opt/synapsis
docker compose pull
docker compose up -d
```
Useful checks:
```bash
docker compose ps
docker compose logs -f app
docker compose images
```
## 5. Which Compose File Is Which
There are two main Docker compose paths in this repo.
### Local source-build compose
File: [docker/docker-compose.yml](/Users/christopher/Dev/Synapsis/Synapsis/docker/docker-compose.yml)
Purpose:
- local Docker testing
- builds from your current working tree
- no GHCR push required
### Production install compose
File: [docker-compose.yml](/Users/christopher/Dev/Synapsis/Synapsis/docker-compose.yml)
Purpose:
- end-user install
- server deployment
- uses `ghcr.io/gnosyslabs/synapsis:latest`
Do not confuse them.
## 6. Recommended Workflow
This is the default path that makes the most sense for Synapsis:
1. Make code changes locally
2. Run `npm run type-check`
3. Run `npm run build`
4. If Docker behavior matters, run `cd docker && docker compose up --build`
5. Keep stacking fixes until the server actually needs them
6. Commit and push
7. Build and push the GHCR image
8. Pull and restart on the server
## 7. When To Publish A New Docker Image
Publish when:
- you want the fix on the real server
- you changed install/runtime/container behavior
- you finished a coherent batch of fixes
Do not publish just because:
- one small UI bug was fixed locally
- one small API bug was fixed and not needed on the server yet
## 8. Current Install Reality
For clean servers, the normal install path is:
```bash
curl -fsSL https://synapsis.social/install.sh | bash
```
For servers that already run nginx or another reverse proxy on `80/443`, use:
```bash
curl -fsSL https://synapsis.social/install.sh | PROXY=none bash
```
In `PROXY=none` mode, Synapsis binds to `127.0.0.1:${PORT}` and your existing reverse proxy should point there.
+51 -130
View File
@@ -2,164 +2,85 @@
**Your node. Your network. Your identity.** **Your node. Your network. Your identity.**
Synapsis is an open-source, federated social network built for the decentralized web. Run your own node, own your identity with DIDs, and communicate across the Swarm network with plain text server-stored messaging. No corporations. No lock-in. Just you and the network. Synapsis is an open-source federated social network built around sovereign DIDs, portable user-owned media storage, and the Swarm network.
--- ## Run a node on a VPS
## 🚀 Deploy Your Own Node (5 Minutes) Synapsis runs directly under systemd. It does not require Docker, PostgreSQL, or a bundled reverse proxy.
Run your personal Synapsis node with a single command: Prerequisites:
- A Linux VPS with systemd
- Node.js 20 or newer
- Git, npm, and OpenSSL
- Your own nginx, Caddy, Traefik, or other reverse proxy
Install from a checkout:
```bash ```bash
# 1. Bootstrap the deployment directory sudo bash deploy/install.sh
curl -fsSL https://synapsis.social/install.sh | bash sudo nano /etc/synapsis.env
sudo bash /opt/synapsis/deploy/update.sh
# 2. Edit /opt/synapsis/.env with your domain and admin email
nano /opt/synapsis/.env
# 3. Start your node
cd /opt/synapsis
docker compose up -d
``` ```
Done! Your node is live at `https://your-domain.com` with automatic SSL. No build step. No dependencies. No fuss. The service binds only to `127.0.0.1:43821`. Point your reverse proxy at that address and terminate TLS there. `PORT` can be overridden in `/etc/synapsis.env` if needed.
Database migrations run automatically on startup and during updates.
Set `DOMAIN` to the canonical host you want Synapsis to live on. If you also create a `www` DNS record, the bundled Caddy setup will redirect `www` to that canonical domain automatically. The installer creates:
If your server already has nginx or another reverse proxy using `80/443`, use the advanced mode instead: - Application checkout: `/opt/synapsis`
- Environment file: `/etc/synapsis.env`
- Embedded Turso database: `/var/lib/synapsis/synapsis.db`
- Service: `synapsis.service`
Useful commands:
```bash ```bash
curl -fsSL https://synapsis.social/install.sh | PROXY=none bash sudo systemctl status synapsis
sudo journalctl -u synapsis -f
sudo /opt/synapsis/deploy/update.sh
sudo /opt/synapsis/deploy/uninstall.sh
``` ```
**Updating (migrations run automatically):** Uninstalling preserves the database and environment by default. Pass `--purge-data` only when you intentionally want to remove both.
```bash
curl -fsSL https://synapsis.social/update.sh | bash
```
**Full uninstall:** ## Storage and account portability
```bash
curl -fsSL https://synapsis.social/uninstall.sh | bash
```
For detailed Docker setup, see [docker/README.md](docker/README.md). The node database is a local embedded Turso/SQLite file. Media remains in each user's S3-compatible bucket so exported accounts can retain portable media URLs and move between Synapsis nodes without requiring the old node to transfer a shared upload directory.
--- Supported S3-compatible providers include AWS S3, Cloudflare R2, Backblaze B2, Wasabi, and Contabo. Synapsis stores each user's credentials encrypted with `AUTH_SECRET`.
## ✨ Features ## Development
- **🌐 Swarm Network** — Native peer-to-peer network with automatic node discovery and gossip protocol
- **💬 Swarm Chat** — Direct messaging across the entire network
- **🔐 Decentralized Identity (DIDs)** — Cryptographic identity you truly own, portable between nodes
- **🤖 AI Bots** — Create AI-powered bot accounts with custom personalities
- **🎨 Modern UI** — Clean, responsive interface inspired by Vercel's design system
- **🖼️ Rich Media** — Image uploads, media galleries, and S3-compatible storage
- **🛡️ Built-in Moderation** — Admin dashboard for user management and content moderation
- **📱 Auto Port Detection** — Runs on the first available port (3000-3020) automatically
- **📊 Curated Feeds** — Smart algorithms highlighting engaging content across the swarm
---
## 📖 Documentation
- **[Docker Deployment Guide](docker/README.md)** — Complete production deployment instructions
- **[User Guide](/guide)** — Learn how Synapsis works (visit after installing)
- **[API Documentation](https://docs.synapsis.social)** — Developer reference
---
## 🏗️ Architecture
Synapsis differs from traditional social networks by prioritizing **sovereign identity** and **native peer-to-peer communication**.
### 🔐 Decentralized Identity (DIDs)
Unlike centralized platforms where your identity is a row in a database owned by a corporation, Synapsis uses a cryptographic identity system:
| Concept | Description |
|---------|-------------|
| **DID** | A unique, cryptographically-generated identifier (`did:key:...`) assigned to every user. This is your true identity that exists independently of any server. |
| **Handle** | A human-readable username (`@alice`) that points to your DID. Think of it like a domain name pointing to an IP address. |
| **Key Pair** | Every account has a public/private key pair. Your private key proves you are you; your public key lets others verify your identity. |
**Why this matters:**
- **Ownership** — Your identity is cryptographically yours, not controlled by a company
- **Authenticity** — Every post is signed with your private key, proving it came from you
- **True Portability** — Move your account between nodes without losing followers
### 🌐 The Swarm Network
Synapsis operates on the **Swarm** — a native peer-to-peer network designed specifically for Synapsis nodes:
- **Gossip Protocol** — Nodes discover each other automatically and exchange information
- **Swarm Timeline** — Aggregated feed of posts from across all Synapsis nodes
- **Swarm Chat** — Direct messaging between users on any Synapsis node
- **Handle Registry** — Distributed directory of user handles across the swarm
- **Instant Interactions** — Likes, reposts, follows, and mentions delivered in real-time
### 🆚 Synapsis vs. Traditional Federation
| Feature | Traditional Federation | Synapsis |
|---------|------------------------|----------|
| **Identity** | Server-bound (`@user@server`) | DID-based (cryptographic, portable) |
| **Account Migration** | Limited (followers don't auto-migrate) | **Full** — DID-based migration with auto-follow |
| **Cryptographic Signing** | HTTP Signatures only | Full post signing with user keys |
| **Direct Messages** | Posts with limited visibility | Direct messaging between users |
| **Network Discovery** | Manual server discovery | Automatic gossip protocol |
| **AI Bots** | Not supported | Native bot framework with LLM integration |
| **Interactions** | Queue-based, delayed | Instant delivery via Swarm |
---
## 🛠️ Development
Want to hack on Synapsis? Here's how to run it locally:
### Prerequisites
- Node.js 20+
- PostgreSQL 15+
- S3-compatible storage (AWS S3, Cloudflare R2, Backblaze B2, Wasabi, or Contabo)
### Local Setup
```bash ```bash
# 1. Clone the repository
git clone https://github.com/GnosysLabs/Synapsis.git git clone https://github.com/GnosysLabs/Synapsis.git
cd synapsis cd Synapsis
# 2. Install dependencies
npm install npm install
# 3. Configure environment
cp .env.example .env cp .env.example .env
# Edit .env with your local database and storage settings npm run db:migrate
# 4. Set up the database
npm run db:push
# 5. Run the development server
npm run dev npm run dev
``` ```
Visit `http://localhost:3000` and register with an email listed in `ADMIN_EMAILS`. Local setup no longer uses a dedicated `/install` route. The development database defaults to `./data/synapsis.db`. No separate database server is needed.
### Tech Stack Common commands:
- **Framework** — [Next.js 15+](https://nextjs.org/) (App Router) ```bash
- **Database** — PostgreSQL with [Drizzle ORM](https://orm.drizzle.team/) npm run type-check
- **Styling** — Tailwind CSS v4 & custom Vercel-like design system npm test
- **Authentication** — Auth.js (NextAuth) npm run db:generate
- **Type Safety** — TypeScript npm run db:migrate
npm run build
```
--- ## Architecture
## 📜 License - **Framework:** Next.js 16 and React 19
- **Database:** embedded Turso with Drizzle ORM's relational-query v2 API
- **Identity:** DIDs and per-user signing keys
- **Media:** user-owned S3-compatible storage
- **Federation:** Synapsis Swarm discovery and signed interactions
- **Deployment:** native Node.js process managed by systemd
Licensed under the **Apache 2.0 License**. See [LICENSE](LICENSE) for details. ## License
--- Licensed under the [Apache License 2.0](LICENSE).
<p align="center">
<strong>Run your node. Join the swarm. Own your social.</strong>
</p>
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/synapsis}"
DATA_DIR="${DATA_DIR:-/var/lib/synapsis}"
ENV_FILE="${ENV_FILE:-/etc/synapsis.env}"
REPO_URL="${REPO_URL:-https://github.com/GnosysLabs/Synapsis.git}"
BRANCH="${BRANCH:-main}"
if [[ ${EUID} -ne 0 ]]; then
echo "Run this installer as root." >&2
exit 1
fi
for command in git node npm openssl runuser systemctl; do
command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; }
done
node_major="$(node -p 'process.versions.node.split(`.`)[0]')"
if (( node_major < 20 )); then
echo "Synapsis requires Node.js 20 or newer." >&2
exit 1
fi
if ! id synapsis >/dev/null 2>&1; then
useradd --system --home-dir "$DATA_DIR" --create-home --shell /usr/sbin/nologin synapsis
fi
install -d -o synapsis -g synapsis -m 0750 "$DATA_DIR"
if [[ -e "$APP_DIR" ]]; then
echo "$APP_DIR already exists; use deploy/update.sh for an existing installation." >&2
exit 1
fi
git clone --branch "$BRANCH" --single-branch "$REPO_URL" "$APP_DIR"
chown -R synapsis:synapsis "$APP_DIR"
if [[ ! -e "$ENV_FILE" ]]; then
auth_secret="$(openssl rand -base64 48 | tr -d '\n')"
port="43821"
install -m 0600 -o root -g synapsis /dev/null "$ENV_FILE"
{
echo "DATABASE_PATH=$DATA_DIR/synapsis.db"
echo "PORT=$port"
echo "AUTH_SECRET=$auth_secret"
echo "ADMIN_EMAILS=admin@example.com"
echo "NEXT_PUBLIC_NODE_DOMAIN=localhost:$port"
} > "$ENV_FILE"
fi
set -a
source "$ENV_FILE"
set +a
runuser -u synapsis -- npm --prefix "$APP_DIR" ci
runuser -u synapsis -- npm --prefix "$APP_DIR" run db:migrate
runuser -u synapsis -- npm --prefix "$APP_DIR" run build
install -m 0644 "$APP_DIR/deploy/synapsis.service" /etc/systemd/system/synapsis.service
systemctl daemon-reload
systemctl enable --now synapsis
echo "Synapsis is listening on http://127.0.0.1:${PORT}"
echo "Edit $ENV_FILE, run $APP_DIR/deploy/update.sh, then configure your own reverse proxy."
+23
View File
@@ -0,0 +1,23 @@
[Unit]
Description=Synapsis node
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=synapsis
Group=synapsis
WorkingDirectory=/opt/synapsis
EnvironmentFile=/etc/synapsis.env
Environment=NODE_ENV=production
ExecStart=/usr/bin/env npm start
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/synapsis /opt/synapsis/.next
[Install]
WantedBy=multi-user.target
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/synapsis}"
DATA_DIR="${DATA_DIR:-/var/lib/synapsis}"
ENV_FILE="${ENV_FILE:-/etc/synapsis.env}"
if [[ ${EUID} -ne 0 ]]; then
echo "Run this uninstaller as root." >&2
exit 1
fi
systemctl disable --now synapsis 2>/dev/null || true
rm -f /etc/systemd/system/synapsis.service
systemctl daemon-reload
rm -rf "$APP_DIR"
if [[ "${1:-}" == "--purge-data" ]]; then
rm -rf "$DATA_DIR"
rm -f "$ENV_FILE"
userdel synapsis 2>/dev/null || true
echo "Synapsis and its database were removed."
else
echo "Synapsis was removed; $DATA_DIR and $ENV_FILE were preserved."
fi
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/synapsis}"
DATA_DIR="${DATA_DIR:-/var/lib/synapsis}"
ENV_FILE="${ENV_FILE:-/etc/synapsis.env}"
if [[ ${EUID} -ne 0 ]]; then
echo "Run this updater as root." >&2
exit 1
fi
[[ -d "$APP_DIR/.git" ]] || { echo "No Synapsis checkout found at $APP_DIR" >&2; exit 1; }
[[ -f "$ENV_FILE" ]] || { echo "Missing $ENV_FILE" >&2; exit 1; }
set -a
source "$ENV_FILE"
set +a
systemctl stop synapsis
trap 'systemctl start synapsis' EXIT
if [[ -f "${DATABASE_PATH:-$DATA_DIR/synapsis.db}" ]]; then
database_file="${DATABASE_PATH:-$DATA_DIR/synapsis.db}"
backup_dir="$DATA_DIR/backups/$(date -u +%Y%m%dT%H%M%SZ)"
install -d -o synapsis -g synapsis -m 0750 "$backup_dir"
cp "$database_file" "$backup_dir/synapsis.db"
for suffix in -wal -shm; do
if [[ -f "${database_file}${suffix}" ]]; then
cp "${database_file}${suffix}" "$backup_dir/synapsis.db${suffix}"
fi
done
chown -R synapsis:synapsis "$backup_dir"
fi
runuser -u synapsis -- git -C "$APP_DIR" pull --ff-only
runuser -u synapsis -- npm --prefix "$APP_DIR" ci
runuser -u synapsis -- npm --prefix "$APP_DIR" run db:migrate
runuser -u synapsis -- npm --prefix "$APP_DIR" run build
systemctl start synapsis
trap - EXIT
echo "Synapsis updated successfully."
-81
View File
@@ -1,81 +0,0 @@
# Synapsis - Docker Compose (Production, External Reverse Proxy)
# Uses pre-built image from GitHub Container Registry
#
# Intended for hosts that already run nginx/Traefik/Caddy on 80/443.
# Synapsis binds only to localhost so your existing proxy can forward to it.
services:
postgres:
image: postgres:16-alpine
container_name: synapsis-db
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER:-synapsis}
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
POSTGRES_DB: ${DB_NAME:-synapsis}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- synapsis-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-synapsis} -d ${DB_NAME:-synapsis}"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 256M
app:
image: ghcr.io/gnosyslabs/synapsis:latest
container_name: synapsis-app
restart: unless-stopped
ports:
- "127.0.0.1:${PORT:-3000}:${PORT:-3000}"
environment:
DATABASE_URL: postgresql://${DB_USER:-synapsis}:${DB_PASSWORD:-changeme}@postgres:5432/${DB_NAME:-synapsis}
AUTH_SECRET: ${AUTH_SECRET}
DOMAIN: ${DOMAIN:-localhost}
NEXT_PUBLIC_NODE_DOMAIN: ${NEXT_PUBLIC_NODE_DOMAIN:-}
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-}
ALLOW_LOCALHOST: ${ALLOW_LOCALHOST:-}
ADMIN_EMAILS: ${ADMIN_EMAILS}
BOT_MAX_PER_USER: ${BOT_MAX_PER_USER:-5}
HOST_UPDATER_SOCKET: ${HOST_UPDATER_SOCKET:-/var/run/synapsis-updater/updater.sock}
HOST_UPDATER_TOKEN: ${HOST_UPDATER_TOKEN:-}
PORT: ${PORT:-3000}
NODE_ENV: production
volumes:
- uploads_data:/app/uploads
- /var/run/synapsis-updater:/var/run/synapsis-updater
networks:
- synapsis-network
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:${PORT:-3000}/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 256M
volumes:
postgres_data:
driver: local
uploads_data:
driver: local
networks:
synapsis-network:
driver: bridge
-149
View File
@@ -1,149 +0,0 @@
# Synapsis - Docker Compose (Production)
# Uses pre-built image from GitHub Container Registry
#
# Quick Start:
# 1. Bootstrap the deployment directory:
# curl -fsSL https://synapsis.social/install.sh | bash
# 2. Configure (edit with your domain and admin email): nano /opt/synapsis/.env
# 3. Start your node: cd /opt/synapsis && docker compose up -d
#
# Services:
# - Synapsis Next.js application (from GHCR)
# - PostgreSQL database (persistent storage)
# - Caddy reverse proxy with automatic SSL
services:
# ============================================
# PostgreSQL Database
# ============================================
postgres:
image: postgres:16-alpine
container_name: synapsis-db
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER:-synapsis}
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
POSTGRES_DB: ${DB_NAME:-synapsis}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- synapsis-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-synapsis} -d ${DB_NAME:-synapsis}"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 256M
# ============================================
# Synapsis Application (from GHCR)
# ============================================
app:
image: ghcr.io/gnosyslabs/synapsis:latest
container_name: synapsis-app
restart: unless-stopped
environment:
# Database connection
DATABASE_URL: postgresql://${DB_USER:-synapsis}:${DB_PASSWORD:-changeme}@postgres:5432/${DB_NAME:-synapsis}
# Authentication
AUTH_SECRET: ${AUTH_SECRET}
# Domain configuration
DOMAIN: ${DOMAIN:-localhost}
NEXT_PUBLIC_NODE_DOMAIN: ${NEXT_PUBLIC_NODE_DOMAIN:-}
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-}
ALLOW_LOCALHOST: ${ALLOW_LOCALHOST:-}
# Admin emails
ADMIN_EMAILS: ${ADMIN_EMAILS}
# Optional settings
BOT_MAX_PER_USER: ${BOT_MAX_PER_USER:-5}
HOST_UPDATER_SOCKET: ${HOST_UPDATER_SOCKET:-/var/run/synapsis-updater/updater.sock}
HOST_UPDATER_TOKEN: ${HOST_UPDATER_TOKEN:-}
# Port configuration (auto or specific port)
PORT: ${PORT:-3000}
# Node environment
NODE_ENV: production
volumes:
# Uploads directory (if using local file storage)
- uploads_data:/app/uploads
# Shared volume for port coordination with Caddy
- port_data:/var/run/synapsis
- /var/run/synapsis-updater:/var/run/synapsis-updater
networks:
- synapsis-network
depends_on:
postgres:
condition: service_healthy
# Increased start period to allow migrations to run
healthcheck:
test: ["CMD-SHELL", "PORT=$$(cat /var/run/synapsis/port 2>/dev/null || echo 3000); case \"$$PORT\" in ''|*[!0-9]*) PORT=3000;; esac; wget -q --spider http://127.0.0.1:$$PORT/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 256M
# ============================================
# Caddy Reverse Proxy (Automatic SSL)
# ============================================
caddy:
image: caddy:2-alpine
container_name: synapsis-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-entrypoint.sh:/usr/local/bin/caddy-entrypoint.sh:ro
- caddy_data:/data
- caddy_config:/config
# Shared volume for port coordination with app
- port_data:/var/run/synapsis
networks:
- synapsis-network
depends_on:
app:
condition: service_healthy
environment:
- DOMAIN=${DOMAIN:-localhost}
# Use custom entrypoint to read dynamic port from app
entrypoint: ["/usr/local/bin/caddy-entrypoint.sh"]
# ============================================
# Persistent Volumes
# ============================================
volumes:
postgres_data:
driver: local
uploads_data:
driver: local
caddy_data:
driver: local
caddy_config:
driver: local
port_data:
driver: local
# ============================================
# Networks
# ============================================
networks:
synapsis-network:
driver: bridge
-96
View File
@@ -1,96 +0,0 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
pnpm-lock.yaml
# Build outputs
.next
out
dist
build
*.tsbuildinfo
# Environment files (these are passed via docker-compose)
.env
.env.local
.env.*.local
.env.dev
.env.test
.env.production
# Version control
.git
.gitignore
.gitattributes
# IDE and editor files
.vscode
.idea
*.swp
*.swo
*~
.DS_Store
Thumbs.db
# Testing
coverage
.nyc_output
*.test.ts
*.test.tsx
*.spec.ts
*.spec.tsx
tests
test
__tests__
# Documentation (not needed in production image)
*.md
!README.md
LICENSE
CHANGELOG.md
CONTRIBUTING.md
# Docker files (avoid recursive copy)
docker
docker-compose*.yml
Dockerfile*
.dockerignore
# CI/CD
.github
.gitlab-ci.yml
.travis.yml
azure-pipelines.yml
Jenkinsfile
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Misc
.cache
temp
tmp
.tmp
uploads/*
!uploads/.gitkeep
# Large binary files
*.mp4
*.mov
*.avi
*.mkv
*.pdf
*.zip
*.tar.gz
*.rar
-51
View File
@@ -1,51 +0,0 @@
# ===========================================
# Synapsis Environment Configuration
# ===========================================
# Copy this file to .env and configure your values
# ===========================================
# REQUIRED: Core Settings
# ===========================================
# Your domain name (no scheme, no path). Example: synapsis.example.com
DOMAIN=your-domain.com
# Optional: Override node domain (host only, defaults to DOMAIN)
# NEXT_PUBLIC_NODE_DOMAIN=your-domain.com
# Optional: Public app URL for background jobs (auto-derived from node domain if unset)
# NEXT_PUBLIC_APP_URL=https://your-domain.com
# Admin email addresses (comma-separated for multiple)
ADMIN_EMAILS=admin@your-domain.com
# Authentication secret - generate with: openssl rand -hex 32
AUTH_SECRET=your-secret-key-here-minimum-32-characters
# ===========================================
# REQUIRED: Database
# ===========================================
DB_USER=synapsis
DB_PASSWORD=your-secure-password-here
DB_NAME=synapsis
# ===========================================
# OPTIONAL: General Settings
# ===========================================
# Application port.
# Default install (with bundled Caddy): PORT=auto scans 3000-3020 automatically.
# Existing reverse proxy mode (PROXY=none): use a fixed localhost port, e.g. PORT=3000.
PORT=auto
# Host updater integration for the admin panel.
# These are generated by the installer for Docker installs.
# HOST_UPDATER_SOCKET=/var/run/synapsis-updater/updater.sock
# HOST_UPDATER_TOKEN=
# Allow localhost domains in production (set only for local testing)
# ALLOW_LOCALHOST=1
# Maximum AI bots per user (default: 5)
# BOT_MAX_PER_USER=5
-44
View File
@@ -1,44 +0,0 @@
# Caddyfile for Synapsis
# Automatic HTTPS via Let's Encrypt
www.{$DOMAIN} {
redir https://{$DOMAIN}{uri} permanent
}
{$DOMAIN} {
# Reverse proxy to Synapsis app (port is set via APP_PORT env var)
reverse_proxy app:{$APP_PORT:3000}
# Enable compression
encode gzip
# Security headers
header {
# Enable HSTS
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Prevent clickjacking
X-Frame-Options "SAMEORIGIN"
# Prevent MIME type sniffing
X-Content-Type-Options "nosniff"
# XSS protection
X-XSS-Protection "1; mode=block"
# Referrer policy
Referrer-Policy "strict-origin-when-cross-origin"
}
# Logging
log {
output file /data/access.log
format json
}
# Increase max body size for uploads (20MB)
request_body {
max_size 20MB
}
}
# Health check endpoint for Docker
:8080 {
respond /health "healthy" 200
}
-133
View File
@@ -1,133 +0,0 @@
# Synapsis - Multi-stage Production Dockerfile
# This Dockerfile creates a production-ready image that:
# - Keeps source files immutable (prevents accidental modifications)
# - Optimizes build size with multi-stage approach
# - Runs as non-root user for security
# ============================================
# Stage 1: Dependencies
# ============================================
FROM node:22-alpine AS deps
RUN apk add --no-cache libc6-compat openssl
WORKDIR /app
# Copy package files for efficient layer caching
COPY package.json package-lock.json* ./
# Install all dependencies (production + dev all in dependencies now)
RUN npm ci --ignore-scripts && npm cache clean --force
# ============================================
# Stage 2: Builder
# ============================================
FROM node:22-alpine AS builder
RUN apk add --no-cache libc6-compat openssl
WORKDIR /app
# Copy dependencies from deps stage
COPY --from=deps /app/node_modules ./node_modules
# Copy all source files
COPY . .
# Set environment variables for build
ARG APP_VERSION=dev
ARG APP_COMMIT=unknown
ARG APP_BUILD_DATE=
ARG APP_GITHUB_URL=
ARG APP_IMAGE_REPO=ghcr.io/gnosyslabs/synapsis
ARG APP_SOURCE_REPO=https://github.com/GnosysLabs/Synapsis
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV APP_VERSION=${APP_VERSION}
ENV APP_COMMIT=${APP_COMMIT}
ENV APP_BUILD_DATE=${APP_BUILD_DATE}
ENV APP_GITHUB_URL=${APP_GITHUB_URL}
ENV APP_IMAGE_REPO=${APP_IMAGE_REPO}
ENV APP_SOURCE_REPO=${APP_SOURCE_REPO}
# Build the Next.js application
RUN npm run build
# ============================================
# Stage 3: Production Dependencies
# ============================================
FROM deps AS prod-deps
RUN npm prune --omit=dev && npm cache clean --force
# ============================================
# Stage 4: Production Runner
# ============================================
FROM node:22-alpine AS runner
RUN apk add --no-cache libc6-compat openssl netcat-openbsd wget
WORKDIR /app
ARG APP_VERSION=dev
ARG APP_COMMIT=unknown
ARG APP_BUILD_DATE=
ARG APP_GITHUB_URL=
ARG APP_IMAGE_REPO=ghcr.io/gnosyslabs/synapsis
ARG APP_SOURCE_REPO=https://github.com/GnosysLabs/Synapsis
LABEL org.opencontainers.image.source="${APP_SOURCE_REPO}"
LABEL org.opencontainers.image.description="Synapsis self-hosted social node"
LABEL org.opencontainers.image.version="${APP_VERSION}"
LABEL org.opencontainers.image.revision="${APP_COMMIT}"
LABEL org.opencontainers.image.created="${APP_BUILD_DATE}"
# Create non-root user for security
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Create shared directory for port file (must be writable by nextjs)
RUN mkdir -p /var/run/synapsis && chown nextjs:nodejs /var/run/synapsis
# Set production environment
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
ENV APP_VERSION=${APP_VERSION}
ENV APP_COMMIT=${APP_COMMIT}
ENV APP_BUILD_DATE=${APP_BUILD_DATE}
ENV APP_GITHUB_URL=${APP_GITHUB_URL}
ENV APP_IMAGE_REPO=${APP_IMAGE_REPO}
ENV APP_SOURCE_REPO=${APP_SOURCE_REPO}
# Copy public assets
COPY --from=builder /app/public ./public
# Copy Next.js build output
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Copy pruned runtime dependencies
COPY --from=prod-deps --chown=nextjs:nodejs /app/node_modules ./node_modules
# Copy drizzle migrations, config, and schema (needed for migrations)
COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle
COPY --from=builder --chown=nextjs:nodejs /app/drizzle.config.ts ./
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./
COPY --from=builder --chown=nextjs:nodejs /app/src/db/schema.ts ./src/db/schema.ts
# Copy entrypoint script
COPY --from=builder --chown=nextjs:nodejs /app/docker/docker-entrypoint.sh ./
RUN chmod +x ./docker-entrypoint.sh
# Switch to non-root user
USER nextjs
# Expose port range for auto-detection
EXPOSE 3000-3020
# Health check - longer start period to allow migrations to run
# Reads dynamic port file when PORT=auto
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \
CMD sh -c 'PORT_FILE=/var/run/synapsis/port; PORT=3000; if [ -f "$PORT_FILE" ]; then PORT=$(cat "$PORT_FILE"); fi; case "$PORT" in ""|*[!0-9]*) PORT=3000;; esac; wget -q --spider "http://127.0.0.1:${PORT}/api/health" || exit 1'
# Set the entrypoint
ENTRYPOINT ["./docker-entrypoint.sh"]
-250
View File
@@ -1,250 +0,0 @@
# Synapsis Docker Deployment
Production Docker deployment using pre-built images from GitHub Container Registry.
---
## 🚀 Quick Start
This is the default install path for a fresh VPS where Synapsis should manage its own HTTPS with Caddy.
```bash
curl -fsSL https://synapsis.social/install.sh | bash
nano /opt/synapsis/.env # Add your domain and admin email
cd /opt/synapsis
docker compose up -d
```
Your node is live at `https://your-domain.com` with automatic SSL.
---
## 📋 What You Need
| Requirement | Details |
|-------------|---------|
| **Server** | 2GB RAM, 2 CPU cores, 20GB SSD (minimum) |
| **Domain** | A domain or subdomain pointing to your server |
| **Docker** | Installed automatically by `install.sh` when missing on supported Linux hosts |
| **Ports** | `80` and `443` must be free for the default Caddy install |
---
## ⚙️ Configuration
Edit `.env` and set these required values (domain should be host only, no scheme or path):
| Variable | What to put |
|----------|-------------|
| `DOMAIN` | Your domain (e.g., `synapsis.example.com`) |
| `DB_PASSWORD` | Strong password for PostgreSQL |
| `AUTH_SECRET` | Run: `openssl rand -hex 32` |
| `ADMIN_EMAILS` | Your email address |
Use the bare/canonical host in `DOMAIN`. Example: set `DOMAIN=synapsis.example.com`, not `www.synapsis.example.com`.
If you also want `www.synapsis.example.com` to work, create a DNS record for `www` pointing to the same server. The bundled Caddy config will redirect `www` to the canonical `DOMAIN`.
Optional (advanced):
- `NEXT_PUBLIC_NODE_DOMAIN` to override the node domain (defaults to `DOMAIN`)
- `NEXT_PUBLIC_APP_URL` to override the public app URL used by background jobs (auto-derived from the node domain)
- `ALLOW_LOCALHOST=1` to allow `localhost` in production containers for local testing
**Port Configuration:**
- `PORT=auto` (default) — Automatically finds an available port between 3000-3020
- `PORT=3000` — Use a specific port instead
---
## Advanced: Existing nginx/Traefik/Caddy Host
If your server already runs a reverse proxy on `80/443`, use the advanced mode:
```bash
curl -fsSL https://synapsis.social/install.sh | PROXY=none bash
nano /opt/synapsis/.env
cd /opt/synapsis
docker compose up -d
```
This mode:
- skips the bundled Caddy service
- binds Synapsis to `127.0.0.1:${PORT:-3000}`
- expects your existing reverse proxy to forward traffic there
In `PROXY=none` mode, `PORT` is the localhost port your reverse proxy should target. The installer automatically changes `PORT=auto` to `PORT=3000` unless you override it.
Example nginx site:
```nginx
server {
server_name node.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
---
## 🔄 Updates (migrations run automatically)
Use the updater for existing installs. It preserves `.env`, refreshes the installed compose/proxy files, pulls the latest published image, and restarts the stack.
```bash
curl -fsSL https://synapsis.social/update.sh | bash
```
If the host was installed with `PROXY=none`, the updater detects that and keeps using the proxyless compose file automatically.
Manual update still works if you only want to restart with the already-installed files:
```bash
cd /opt/synapsis
docker compose pull
docker compose up -d
```
## 🗑️ Full Uninstall
To remove Synapsis completely from a host and start over:
```bash
curl -fsSL https://synapsis.social/uninstall.sh | bash
```
The uninstaller destroys the Synapsis containers, volumes, network, cached Synapsis images, and `/opt/synapsis`. It requires typing `DELETE` unless you set `FORCE=1`.
---
## 🛠️ Common Commands
```bash
# View logs
docker compose logs -f app
# Restart services
docker compose restart app
# Stop everything
docker compose down
# Database backup
docker compose exec postgres pg_dump -U synapsis synapsis > backup.sql
# Access database
docker compose exec postgres psql -U synapsis -d synapsis
```
---
## 🔍 Troubleshooting
### Container won't start
```bash
docker compose config # Validate config
docker compose logs app --tail=50 # Check errors
```
### Port already in use
If the installer says `80` or `443` is already in use, another reverse proxy is already bound there.
Use a fresh VPS for the default Caddy install, or rerun the installer in advanced mode:
```bash
curl -fsSL https://synapsis.social/install.sh | PROXY=none bash
```
For the application port itself, `PORT=auto` (default) automatically finds an available port. If you set a specific port that's taken:
```bash
# Check what's using the port
sudo netstat -tlnp | grep :3000
# Choose a different fixed port in /opt/synapsis/.env
# Example: PORT=3013
```
### Database connection failed
```bash
# Check database health
docker compose ps
# Verify environment variables loaded
docker compose exec app env | grep DATABASE
```
### SSL certificate issues
```bash
# Check Caddy logs
docker compose logs caddy
# Test Caddy config
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
```
### Image pull fails
```bash
# Verify image exists
docker pull ghcr.io/gnosyslabs/synapsis:latest
# Check the published package tags in GitHub Container Registry
```
---
## 💾 Backup Strategy
Create `/opt/synapsis/backup.sh`:
```bash
#!/bin/bash
BACKUP_DIR="/var/backups/synapsis"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
# Database backup
docker compose exec -T postgres pg_dump -U synapsis synapsis > "$BACKUP_DIR/db_$DATE.sql"
echo "✅ Backup complete: $DATE"
```
Schedule daily backups:
```bash
chmod +x /opt/synapsis/backup.sh
echo "0 2 * * * /opt/synapsis/backup.sh" | sudo crontab -
```
---
## 🏗️ Building from Source
To build locally instead of using pre-built images:
```bash
git clone https://github.com/GnosysLabs/Synapsis.git
cd synapsis/docker
docker compose up -d --build
```
To publish a stamped GHCR image with embedded app version, commit, and build date:
```bash
cd /path/to/Synapsis
./scripts/docker-publish.sh
```
That script automatically publishes:
- `ghcr.io/gnosyslabs/synapsis:latest`
- `ghcr.io/gnosyslabs/synapsis:<YYYY.MM.DD.N>`
- `ghcr.io/gnosyslabs/synapsis:<short-sha>`
---
For full documentation, visit [docs.synapsis.social](https://docs.synapsis.social)
Binary file not shown.
-58
View File
@@ -1,58 +0,0 @@
#!/bin/sh
# Caddy Entrypoint Script for Synapsis
# Reads the dynamic port from the shared file and starts Caddy
set -e
SHARED_PORT_FILE="/var/run/synapsis/port"
DEFAULT_PORT=3000
# Normalize DOMAIN (strip scheme/path) to avoid invalid Caddyfile hostnames
sanitize_domain() {
echo "$1" | sed -E 's#^[a-zA-Z]+://##; s#/.*$##'
}
if [ -n "$DOMAIN" ]; then
CLEAN_DOMAIN=$(sanitize_domain "$DOMAIN")
if [ "$CLEAN_DOMAIN" != "$DOMAIN" ]; then
export DOMAIN="$CLEAN_DOMAIN"
echo "🌐 DOMAIN normalized to $DOMAIN"
fi
fi
if [ -z "$DOMAIN" ]; then
echo "❌ DOMAIN is not set. Exiting."
exit 1
fi
# Wait for the port file to exist (with timeout)
echo "⏳ Waiting for Synapsis app to announce its port..."
TIMEOUT=60
ELAPSED=0
while [ ! -f "$SHARED_PORT_FILE" ] && [ $ELAPSED -lt $TIMEOUT ]; do
sleep 1
ELAPSED=$((ELAPSED + 1))
done
if [ -f "$SHARED_PORT_FILE" ]; then
APP_PORT=$(cat "$SHARED_PORT_FILE")
echo "✅ Found Synapsis app on port: $APP_PORT"
else
echo "⚠️ Port file not found after ${TIMEOUT}s, using default port: $DEFAULT_PORT"
APP_PORT=$DEFAULT_PORT
fi
export APP_PORT
# Validate that APP_PORT is a number
if ! echo "$APP_PORT" | grep -qE '^[0-9]+$'; then
echo "❌ Invalid port number: $APP_PORT"
exit 1
fi
echo "🌐 Starting Caddy reverse proxy to app:$APP_PORT..."
echo ""
# Start Caddy with the environment variable set
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
-144
View File
@@ -1,144 +0,0 @@
# Synapsis - Docker Compose
# One-command deployment: docker-compose up -d
#
# Services:
# - Synapsis Next.js application
# - PostgreSQL database (persistent storage)
# - Caddy reverse proxy with automatic SSL
#
# All state is stored in Docker volumes - source code is immutable in the image
services:
# ============================================
# PostgreSQL Database
# ============================================
postgres:
image: postgres:16-alpine
container_name: synapsis-db
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER:-synapsis}
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
POSTGRES_DB: ${DB_NAME:-synapsis}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- synapsis-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-synapsis} -d ${DB_NAME:-synapsis}"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 256M
# ============================================
# Synapsis Application
# ============================================
app:
build:
context: ..
dockerfile: docker/Dockerfile
image: synapsis:latest
container_name: synapsis-app
restart: unless-stopped
environment:
# Database connection
DATABASE_URL: postgresql://${DB_USER:-synapsis}:${DB_PASSWORD:-changeme}@postgres:5432/${DB_NAME:-synapsis}
# Authentication
AUTH_SECRET: ${AUTH_SECRET}
# Domain configuration
DOMAIN: ${DOMAIN:-localhost}
NEXT_PUBLIC_NODE_DOMAIN: ${NEXT_PUBLIC_NODE_DOMAIN:-}
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-}
ALLOW_LOCALHOST: ${ALLOW_LOCALHOST:-}
# Admin emails
ADMIN_EMAILS: ${ADMIN_EMAILS}
# Optional settings
BOT_MAX_PER_USER: ${BOT_MAX_PER_USER:-5}
# Port configuration (auto or specific port)
PORT: ${PORT:-3000}
# Node environment
NODE_ENV: production
volumes:
# Uploads directory (if using local file storage)
- uploads_data:/app/uploads
# Shared volume for port coordination with Caddy
- port_data:/var/run/synapsis
networks:
- synapsis-network
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "PORT=$$(cat /var/run/synapsis/port 2>/dev/null || echo 3000); case \"$$PORT\" in ''|*[!0-9]*) PORT=3000;; esac; wget -q --spider http://127.0.0.1:$$PORT/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 256M
# ============================================
# Caddy Reverse Proxy (Automatic SSL)
# ============================================
caddy:
image: caddy:2-alpine
container_name: synapsis-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-entrypoint.sh:/usr/local/bin/caddy-entrypoint.sh:ro
- caddy_data:/data
- caddy_config:/config
# Shared volume for port coordination with app
- port_data:/var/run/synapsis
networks:
- synapsis-network
depends_on:
app:
condition: service_healthy
environment:
- DOMAIN=${DOMAIN:-localhost}
# Use custom entrypoint to read dynamic port from app
entrypoint: ["/usr/local/bin/caddy-entrypoint.sh"]
# ============================================
# Persistent Volumes
# ============================================
volumes:
postgres_data:
driver: local
uploads_data:
driver: local
caddy_data:
driver: local
caddy_config:
driver: local
port_data:
driver: local
# ============================================
# Networks
# ============================================
networks:
synapsis-network:
driver: bridge
-213
View File
@@ -1,213 +0,0 @@
#!/bin/sh
# Synapsis Docker Entrypoint Script
# Handles database migrations, port detection, and application startup
set -e
# ============================================
# Port Detection Configuration
# ============================================
PORT_START=${PORT_START:-3000}
PORT_END=${PORT_END:-3020}
# Function to check if a port is available
check_port_available() {
local port=$1
if ! nc -z localhost "$port" 2>/dev/null; then
return 0 # Port is available
else
return 1 # Port is in use
fi
}
# Function to find first available port in range
find_available_port() {
local start=$1
local end=$2
for port in $(seq "$start" "$end"); do
if check_port_available "$port"; then
echo "$port"
return 0
fi
done
echo "ERROR: No available ports found in range $start-$end" >&2
return 1
}
# Handle PORT=auto
if [ "${PORT}" = "auto" ]; then
echo "🔍 PORT=auto detected, scanning for available port in range ${PORT_START}-${PORT_END}..."
DETECTED_PORT=$(find_available_port "$PORT_START" "$PORT_END")
if [ $? -ne 0 ]; then
echo "❌ Failed to find an available port. Exiting."
exit 1
fi
export PORT="$DETECTED_PORT"
echo "✅ Using automatically detected port: $PORT"
else
echo "📡 Using configured port: $PORT"
fi
# Ensure PORT is set
if [ -z "$PORT" ]; then
echo "⚠️ PORT not set, defaulting to 3000"
export PORT=3000
fi
# Write port to shared file for Caddy to read
SHARED_PORT_FILE="/var/run/synapsis/port"
mkdir -p "$(dirname "$SHARED_PORT_FILE")"
echo "$PORT" > "$SHARED_PORT_FILE"
echo "📝 Port $PORT written to $SHARED_PORT_FILE"
# Export HOSTNAME for Next.js
export HOSTNAME="0.0.0.0"
# ============================================
# Database Migrations
# ============================================
echo ""
echo "========================================"
echo " Synapsis - Starting Application"
echo "========================================"
echo " Time: $(date)"
echo " Working Dir: $(pwd)"
echo " Database URL: ${DATABASE_URL%%:*}://***@***"
echo " Port: $PORT"
echo "========================================"
# Ensure DATABASE_URL is set
if [ -z "$DATABASE_URL" ]; then
echo "❌ DATABASE_URL is not set. Exiting."
exit 1
fi
# Normalize domain inputs (strip scheme/path) to avoid localhost/protocol issues
sanitize_domain() {
echo "$1" | sed -E 's#^[a-zA-Z]+://##; s#/.*$##'
}
# Normalize DOMAIN if provided
if [ -n "$DOMAIN" ]; then
CLEAN_DOMAIN=$(sanitize_domain "$DOMAIN")
if [ "$CLEAN_DOMAIN" != "$DOMAIN" ]; then
export DOMAIN="$CLEAN_DOMAIN"
echo "🌐 DOMAIN normalized to $DOMAIN"
fi
fi
# Normalize NEXT_PUBLIC_NODE_DOMAIN if provided
if [ -n "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
CLEAN_NODE_DOMAIN=$(sanitize_domain "$NEXT_PUBLIC_NODE_DOMAIN")
if [ "$CLEAN_NODE_DOMAIN" != "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
export NEXT_PUBLIC_NODE_DOMAIN="$CLEAN_NODE_DOMAIN"
echo "🌐 NEXT_PUBLIC_NODE_DOMAIN normalized to $NEXT_PUBLIC_NODE_DOMAIN"
fi
fi
# Ensure NEXT_PUBLIC_NODE_DOMAIN is set (fallback to DOMAIN)
if [ -z "$NEXT_PUBLIC_NODE_DOMAIN" ] && [ -n "$DOMAIN" ]; then
export NEXT_PUBLIC_NODE_DOMAIN="$DOMAIN"
echo "🌐 NEXT_PUBLIC_NODE_DOMAIN set to $NEXT_PUBLIC_NODE_DOMAIN"
fi
# Fail fast if running in production with localhost domain
if [ "$NODE_ENV" = "production" ]; then
case "$NEXT_PUBLIC_NODE_DOMAIN" in
""|localhost|localhost:*|127.0.0.1|127.0.0.1:*)
if [ -z "$ALLOW_LOCALHOST" ]; then
echo "❌ NEXT_PUBLIC_NODE_DOMAIN is set to localhost in production."
echo " Set DOMAIN to your public domain in .env, or set ALLOW_LOCALHOST=1 to bypass."
exit 1
fi
;;
esac
fi
# Ensure NEXT_PUBLIC_APP_URL is set for background jobs
if [ -z "$NEXT_PUBLIC_APP_URL" ] && [ -n "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
case "$NEXT_PUBLIC_NODE_DOMAIN" in
http://*|https://*)
NEXT_PUBLIC_APP_URL="$NEXT_PUBLIC_NODE_DOMAIN"
;;
localhost*|127.0.0.1*)
NEXT_PUBLIC_APP_URL="http://$NEXT_PUBLIC_NODE_DOMAIN"
;;
*)
NEXT_PUBLIC_APP_URL="https://$NEXT_PUBLIC_NODE_DOMAIN"
;;
esac
export NEXT_PUBLIC_APP_URL
echo "🌐 NEXT_PUBLIC_APP_URL set to $NEXT_PUBLIC_APP_URL"
fi
# Function to wait for database
wait_for_db() {
echo ""
echo "⏳ Waiting for PostgreSQL..."
# Extract host and port from DATABASE_URL
DB_HOST=$(echo "$DATABASE_URL" | sed -n 's#.*@\([^/:]*\).*#\1#p')
DB_PORT=$(echo "$DATABASE_URL" | sed -n 's#.*:\([0-9][0-9]*\)/.*#\1#p')
DB_HOST=${DB_HOST:-postgres}
DB_PORT=${DB_PORT:-5432}
max_retries=30
retry_count=0
while ! nc -z "$DB_HOST" "$DB_PORT" 2>/dev/null; do
retry_count=$((retry_count + 1))
if [ $retry_count -ge $max_retries ]; then
echo "❌ Failed to connect to database after $max_retries attempts"
exit 1
fi
echo " Attempt $retry_count/$max_retries - waiting for $DB_HOST:$DB_PORT..."
sleep 2
done
echo "✅ Database is ready!"
}
# Function to run database migrations
run_migrations() {
echo ""
echo "🔄 Running database migrations..."
echo " Current directory: $(pwd)"
echo " Drizzle directory contents:"
ls -la drizzle/ 2>/dev/null || echo " (drizzle dir not found or empty)"
# Run migrations using npm script
echo " Executing: npm run db:push"
if npm run db:push 2>&1; then
echo "✅ Migration step complete"
else
echo "❌ Migration failed"
exit 1
fi
}
# Wait for database
wait_for_db
# Run migrations
run_migrations
# Final startup message
echo ""
echo "========================================"
echo " 🚀 Starting Synapsis Server"
echo "========================================"
echo " Environment: $NODE_ENV"
echo " Port: $PORT"
echo " Node Version: $(node --version)"
echo "========================================"
echo ""
# Start the application
exec node server.js
-357
View File
@@ -1,357 +0,0 @@
#!/usr/bin/env python3
import json
import os
import signal
import socketserver
import subprocess
import threading
import time
from datetime import datetime, timezone
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler
SOCKET_PATH = os.environ.get("HOST_UPDATER_SOCKET", "/var/run/synapsis-updater/updater.sock")
TOKEN = os.environ.get("HOST_UPDATER_TOKEN", "")
STATUS_FILE = os.environ.get("HOST_UPDATER_STATUS_FILE", "/opt/synapsis/updater-status.json")
CONFIG_FILE = os.environ.get("HOST_UPDATER_CONFIG_FILE", "/opt/synapsis/updater-config.json")
LOG_FILE = os.environ.get("HOST_UPDATER_LOG_FILE", "/opt/synapsis/updater.log")
UPDATE_SCRIPT = os.environ.get("HOST_UPDATER_SCRIPT", "/opt/synapsis/update-local.sh")
INSTALL_DIR = os.environ.get("INSTALL_DIR", "/opt/synapsis")
AUTO_UPDATE_INTERVAL_MINUTES = max(5, int(os.environ.get("HOST_UPDATER_INTERVAL_MINUTES", "30")))
status_lock = threading.RLock()
current_process = None
last_auto_trigger_at = 0.0
def now_iso():
return datetime.now(timezone.utc).isoformat()
def ensure_parent_dirs():
os.makedirs(os.path.dirname(SOCKET_PATH), exist_ok=True)
os.makedirs(os.path.dirname(STATUS_FILE), exist_ok=True)
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
def default_config():
return {
"autoUpdateEnabled": True,
"intervalMinutes": AUTO_UPDATE_INTERVAL_MINUTES,
}
def load_config():
if not os.path.exists(CONFIG_FILE):
config = default_config()
save_config(config)
return config
with open(CONFIG_FILE, "r", encoding="utf-8") as handle:
stored = json.load(handle)
config = default_config()
config.update({
"autoUpdateEnabled": bool(stored.get("autoUpdateEnabled", True)),
"intervalMinutes": max(5, int(stored.get("intervalMinutes", AUTO_UPDATE_INTERVAL_MINUTES))),
})
return config
def save_config(config):
with open(CONFIG_FILE, "w", encoding="utf-8") as handle:
json.dump(config, handle, indent=2)
def update_config(**changes):
config = load_config()
config.update(changes)
config["autoUpdateEnabled"] = bool(config.get("autoUpdateEnabled", True))
config["intervalMinutes"] = max(5, int(config.get("intervalMinutes", AUTO_UPDATE_INTERVAL_MINUTES)))
save_config(config)
return config
def load_status():
if not os.path.exists(STATUS_FILE):
return {
"available": True,
"status": "idle",
"message": "Ready to update.",
"lastStartedAt": None,
"lastFinishedAt": None,
"lastExitCode": None,
"lastError": None,
"pid": None,
}
with open(STATUS_FILE, "r", encoding="utf-8") as handle:
return json.load(handle)
def normalize_status_on_startup():
status = load_status()
if status.get("status") == "error" and status.get("lastExitCode") == -15:
status.update({
"status": "success",
"message": "Synapsis update completed.",
"lastExitCode": 0,
"lastError": None,
"pid": None,
})
save_status(status)
if status.get("status") == "updating":
status.update({
"status": "idle",
"message": "Ready to update.",
"lastError": None,
"pid": None,
})
save_status(status)
def save_status(status):
with open(STATUS_FILE, "w", encoding="utf-8") as handle:
json.dump(status, handle, indent=2)
def update_status(**changes):
with status_lock:
status = load_status()
status.update(changes)
save_status(status)
return status
def get_status_payload():
status = load_status()
status["config"] = load_config()
return status
def is_authorized(headers):
expected = f"Bearer {TOKEN}"
return bool(TOKEN) and headers.get("Authorization", "") == expected
def watch_process(process):
global current_process
exit_code = process.wait()
with status_lock:
current_process = None
update_status(
status="success" if exit_code == 0 else "error",
message="Synapsis update completed." if exit_code == 0 else "Synapsis update failed.",
lastFinishedAt=now_iso(),
lastExitCode=exit_code,
lastError=None if exit_code == 0 else f"Updater exited with code {exit_code}",
pid=None,
)
def start_update_process(trigger="manual"):
global current_process
# Give the app enough time to return the 202 response before the container restarts.
time.sleep(2)
with status_lock:
log_handle = open(LOG_FILE, "a", encoding="utf-8")
current_process = subprocess.Popen(
[UPDATE_SCRIPT],
cwd=INSTALL_DIR,
stdout=log_handle,
stderr=subprocess.STDOUT,
start_new_session=True,
env={
**os.environ,
"INSTALL_DIR": INSTALL_DIR,
},
)
update_status(pid=current_process.pid, trigger=trigger)
thread = threading.Thread(target=watch_process, args=(current_process,), daemon=True)
thread.start()
def schedule_update(trigger="manual"):
with status_lock:
if current_process and current_process.poll() is None:
return False
update_status(
status="updating",
message="Synapsis update scheduled." if trigger == "manual" else "Automatic Synapsis update scheduled.",
lastStartedAt=now_iso(),
lastFinishedAt=None,
lastExitCode=None,
lastError=None,
pid=None,
trigger=trigger,
)
thread = threading.Thread(target=start_update_process, args=(trigger,), daemon=True)
thread.start()
return True
def auto_update_loop():
global last_auto_trigger_at
while True:
time.sleep(60)
try:
config = load_config()
if not config.get("autoUpdateEnabled", True):
continue
interval_seconds = max(300, int(config.get("intervalMinutes", AUTO_UPDATE_INTERVAL_MINUTES)) * 60)
now = time.time()
with status_lock:
if current_process and current_process.poll() is None:
continue
last_reference = last_auto_trigger_at
status = load_status()
if not last_reference:
last_started = status.get("lastStartedAt")
if last_started:
try:
last_reference = datetime.fromisoformat(last_started).timestamp()
except Exception:
last_reference = 0.0
if last_reference and (now - last_reference) < interval_seconds:
continue
last_auto_trigger_at = now
schedule_update(trigger="auto")
except Exception as error:
update_status(lastError=f"Auto-update scheduler error: {error}")
class ThreadedUnixServer(socketserver.ThreadingMixIn, socketserver.UnixStreamServer):
daemon_threads = True
class Handler(BaseHTTPRequestHandler):
server_version = "SynapsisHostUpdater/1.0"
def log_message(self, format, *args):
return
def send_json(self, status_code, payload):
body = json.dumps(payload).encode("utf-8")
self.send_response(status_code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/health":
self.send_json(HTTPStatus.OK, {"ok": True})
return
if self.path == "/status":
if not is_authorized(self.headers):
self.send_json(HTTPStatus.UNAUTHORIZED, {"error": "Unauthorized"})
return
self.send_json(HTTPStatus.OK, get_status_payload())
return
self.send_json(HTTPStatus.NOT_FOUND, {"error": "Not found"})
def do_POST(self):
global current_process
if self.path != "/update":
self.send_json(HTTPStatus.NOT_FOUND, {"error": "Not found"})
return
if not is_authorized(self.headers):
self.send_json(HTTPStatus.UNAUTHORIZED, {"error": "Unauthorized"})
return
if not schedule_update(trigger="manual"):
self.send_json(
HTTPStatus.CONFLICT,
{
"ok": False,
"status": "updating",
"message": "An update is already running.",
},
)
return
self.send_json(
HTTPStatus.ACCEPTED,
{
"ok": True,
"status": "updating",
"message": "Synapsis update scheduled. The node will restart shortly.",
},
)
def do_PATCH(self):
if self.path != "/config":
self.send_json(HTTPStatus.NOT_FOUND, {"error": "Not found"})
return
if not is_authorized(self.headers):
self.send_json(HTTPStatus.UNAUTHORIZED, {"error": "Unauthorized"})
return
content_length = int(self.headers.get("Content-Length", "0") or "0")
raw_body = self.rfile.read(content_length) if content_length > 0 else b"{}"
try:
payload = json.loads(raw_body.decode("utf-8") or "{}")
except json.JSONDecodeError:
self.send_json(HTTPStatus.BAD_REQUEST, {"error": "Invalid JSON"})
return
if "autoUpdateEnabled" not in payload:
self.send_json(HTTPStatus.BAD_REQUEST, {"error": "autoUpdateEnabled is required"})
return
config = update_config(autoUpdateEnabled=payload.get("autoUpdateEnabled"))
self.send_json(HTTPStatus.OK, {"ok": True, "config": config})
def cleanup_socket(*_args):
try:
if os.path.exists(SOCKET_PATH):
os.remove(SOCKET_PATH)
finally:
raise SystemExit(0)
def main():
ensure_parent_dirs()
if os.path.exists(SOCKET_PATH):
os.remove(SOCKET_PATH)
normalize_status_on_startup()
load_config()
signal.signal(signal.SIGTERM, cleanup_socket)
signal.signal(signal.SIGINT, cleanup_socket)
update_status(available=True)
threading.Thread(target=auto_update_loop, daemon=True).start()
with ThreadedUnixServer(SOCKET_PATH, Handler) as server:
os.chmod(SOCKET_PATH, 0o666)
server.serve_forever()
if __name__ == "__main__":
main()
-365
View File
@@ -1,365 +0,0 @@
#!/bin/sh
set -eu
REPO="${REPO:-GnosysLabs/Synapsis}"
REF="${REF:-main}"
INSTALL_DIR="${1:-${INSTALL_DIR:-/opt/synapsis}}"
PUBLIC_INSTALL_URL="${PUBLIC_INSTALL_URL:-https://synapsis.social/install.sh}"
PROXY="${PROXY:-caddy}"
HOST_UPDATER_SOCKET_PATH="${HOST_UPDATER_SOCKET_PATH:-/var/run/synapsis-updater/updater.sock}"
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "❌ Required command not found: $1" >&2
exit 1
fi
}
download_file() {
source_path="$1"
target_path="$2"
echo "⬇️ Downloading ${source_path}"
curl -fsSL "${RAW_BASE}/${source_path}" -o "${target_path}"
}
normalize_proxy_mode() {
case "$1" in
caddy|none)
printf '%s\n' "$1"
;;
*)
echo "❌ Unsupported PROXY mode: $1" >&2
echo " Supported values: caddy, none" >&2
exit 1
;;
esac
}
is_port_in_use() {
port="$1"
if command -v ss >/dev/null 2>&1; then
ss -tulpn 2>/dev/null | grep -q ":${port} "
return $?
fi
if command -v lsof >/dev/null 2>&1; then
lsof -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1
return $?
fi
if command -v netstat >/dev/null 2>&1; then
netstat -tulpn 2>/dev/null | grep -q ":${port} "
return $?
fi
return 1
}
ensure_caddy_ports_available() {
for port in 80 443; do
if is_port_in_use "$port"; then
echo "❌ Port ${port} is already in use on this host." >&2
echo " The default Synapsis install includes Caddy and needs ports 80/443." >&2
echo " If this is a fresh VPS, free those ports and rerun the installer." >&2
echo " If this host already runs nginx or another reverse proxy, rerun with:" >&2
echo " curl -fsSL ${PUBLIC_INSTALL_URL} | PROXY=none bash" >&2
exit 1
fi
done
}
set_env_value() {
file="$1"
key="$2"
value="$3"
escaped_value=$(printf '%s\n' "$value" | sed -e 's/[\/&]/\\&/g')
sed -i.bak -E "s|^${key}=.*$|${key}=${escaped_value}|" "$file"
rm -f "${file}.bak"
}
ensure_env_value() {
file="$1"
key="$2"
value="$3"
if grep -q "^${key}=" "$file" 2>/dev/null; then
set_env_value "$file" "$key" "$value"
return
fi
printf '%s=%s\n' "$key" "$value" >> "$file"
}
get_env_value() {
file="$1"
key="$2"
if [ ! -f "$file" ]; then
return 1
fi
sed -n -E "s/^${key}=(.*)$/\\1/p" "$file" | head -n 1
}
generate_db_password() {
openssl rand -base64 24 | tr -d '\n' | tr '/+' '_-' | cut -c1-32
}
generate_token() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
return
fi
date +%s | sha256sum | awk '{print $1}'
}
find_available_port() {
start="${1:-3000}"
end="${2:-3020}"
port="$start"
while [ "$port" -le "$end" ]; do
if ! is_port_in_use "$port"; then
printf '%s\n' "$port"
return 0
fi
port=$((port + 1))
done
echo "❌ No available localhost port found in range ${start}-${end} for PROXY=none." >&2
echo " Set PORT explicitly when rerunning the installer if you want a different range." >&2
exit 1
}
resolve_proxyless_port() {
if [ -n "${PORT:-}" ] && [ "${PORT}" != "auto" ]; then
printf '%s\n' "${PORT}"
return
fi
find_available_port 3000 3020
}
install_docker_if_needed() {
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
echo "🐳 Docker is already installed"
return
fi
if [ "$(id -u)" -ne 0 ]; then
echo "❌ Docker is not installed and this installer is not running as root." >&2
echo " Re-run it as root or with sudo so Synapsis can install Docker for you." >&2
exit 1
fi
echo "🐳 Docker not found, installing Docker"
curl -fsSL https://get.docker.com | sh
if command -v systemctl >/dev/null 2>&1; then
systemctl enable --now docker >/dev/null 2>&1 || true
elif command -v service >/dev/null 2>&1; then
service docker start >/dev/null 2>&1 || true
fi
if ! command -v docker >/dev/null 2>&1; then
echo "❌ Docker installation did not finish successfully." >&2
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
echo "⚠️ Docker was installed, but 'docker compose' is not available yet." >&2
echo " Install the Docker Compose plugin before starting Synapsis." >&2
fi
}
install_python3_if_needed() {
if command -v python3 >/dev/null 2>&1; then
return
fi
if command -v apt-get >/dev/null 2>&1; then
apt-get update
apt-get install -y python3
return
fi
if command -v apk >/dev/null 2>&1; then
apk add --no-cache python3
return
fi
if command -v dnf >/dev/null 2>&1; then
dnf install -y python3
return
fi
if command -v yum >/dev/null 2>&1; then
yum install -y python3
return
fi
echo "⚠️ Could not install python3 automatically. Host updater will be unavailable." >&2
}
install_host_updater() {
if [ "$(id -u)" -ne 0 ]; then
echo "⚠️ Host updater installation requires root. Skipping one-click updater setup."
return
fi
download_file "docker/host-updater.py" "${INSTALL_DIR}/host-updater.py"
download_file "docker/update-local.sh" "${INSTALL_DIR}/update-local.sh"
chmod 755 "${INSTALL_DIR}/host-updater.py" "${INSTALL_DIR}/update-local.sh"
mkdir -p "$(dirname "${HOST_UPDATER_SOCKET_PATH}")"
updater_token="$(get_env_value "${INSTALL_DIR}/.env" "HOST_UPDATER_TOKEN" || true)"
if [ -z "${updater_token}" ]; then
updater_token="$(generate_token)"
ensure_env_value "${INSTALL_DIR}/.env" "HOST_UPDATER_TOKEN" "${updater_token}"
fi
ensure_env_value "${INSTALL_DIR}/.env" "HOST_UPDATER_SOCKET" "${HOST_UPDATER_SOCKET_PATH}"
if ! command -v systemctl >/dev/null 2>&1; then
echo "⚠️ systemd not found. Admin one-click updates will be unavailable on this host."
return
fi
install_python3_if_needed
if ! command -v python3 >/dev/null 2>&1; then
echo "⚠️ python3 is unavailable. Admin one-click updates will be unavailable."
return
fi
cat > "${INSTALL_DIR}/updater.env" <<EOF
HOST_UPDATER_TOKEN=${updater_token}
REPO=${REPO}
REF=${REF}
EOF
chmod 600 "${INSTALL_DIR}/updater.env"
cat > /etc/systemd/system/synapsis-updater.service <<EOF
[Unit]
Description=Synapsis Host Updater
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=${INSTALL_DIR}
Environment=INSTALL_DIR=${INSTALL_DIR}
Environment=HOST_UPDATER_SOCKET=${HOST_UPDATER_SOCKET_PATH}
Environment=HOST_UPDATER_STATUS_FILE=${INSTALL_DIR}/updater-status.json
Environment=HOST_UPDATER_LOG_FILE=${INSTALL_DIR}/updater.log
Environment=HOST_UPDATER_SCRIPT=${INSTALL_DIR}/update-local.sh
EnvironmentFile=${INSTALL_DIR}/updater.env
ExecStart=/usr/bin/env python3 ${INSTALL_DIR}/host-updater.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now synapsis-updater.service >/dev/null 2>&1 || true
systemctl restart synapsis-updater.service >/dev/null 2>&1 || true
}
require_command curl
require_command chmod
require_command mkdir
require_command cp
RAW_BASE="https://raw.githubusercontent.com/${REPO}/${REF}"
PROXY="$(normalize_proxy_mode "${PROXY}")"
PROXYLESS_PORT="$(resolve_proxyless_port)"
echo "========================================"
echo " Synapsis Docker Installer"
echo "========================================"
echo " Repo: ${REPO}"
echo " Ref: ${REF}"
echo " Install dir: ${INSTALL_DIR}"
echo " Proxy mode: ${PROXY}"
echo "========================================"
install_docker_if_needed
mkdir -p "${INSTALL_DIR}"
download_file "docker/.env.example" "${INSTALL_DIR}/.env.example"
case "${PROXY}" in
caddy)
ensure_caddy_ports_available
download_file "docker-compose.yml" "${INSTALL_DIR}/docker-compose.yml"
download_file "docker/Caddyfile" "${INSTALL_DIR}/Caddyfile"
download_file "docker/caddy-entrypoint.sh" "${INSTALL_DIR}/caddy-entrypoint.sh"
chmod 755 "${INSTALL_DIR}/caddy-entrypoint.sh"
rm -f "${INSTALL_DIR}/docker-compose.proxyless.yml"
;;
none)
download_file "docker-compose.proxyless.yml" "${INSTALL_DIR}/docker-compose.yml"
rm -f "${INSTALL_DIR}/Caddyfile" "${INSTALL_DIR}/caddy-entrypoint.sh"
;;
esac
if [ ! -f "${INSTALL_DIR}/.env" ]; then
cp "${INSTALL_DIR}/.env.example" "${INSTALL_DIR}/.env"
echo "📝 Created ${INSTALL_DIR}/.env"
if command -v openssl >/dev/null 2>&1; then
set_env_value "${INSTALL_DIR}/.env" "AUTH_SECRET" "$(openssl rand -hex 32)"
set_env_value "${INSTALL_DIR}/.env" "DB_PASSWORD" "$(generate_db_password)"
echo "🔐 Generated AUTH_SECRET and DB_PASSWORD"
else
echo "⚠️ openssl not found, leaving placeholder secrets in .env"
fi
if [ -n "${DOMAIN:-}" ]; then
set_env_value "${INSTALL_DIR}/.env" "DOMAIN" "${DOMAIN}"
echo "🌐 Set DOMAIN=${DOMAIN}"
fi
if [ -n "${ADMIN_EMAILS:-}" ]; then
set_env_value "${INSTALL_DIR}/.env" "ADMIN_EMAILS" "${ADMIN_EMAILS}"
echo "📧 Set ADMIN_EMAILS=${ADMIN_EMAILS}"
fi
if [ "${PROXY}" = "none" ]; then
set_env_value "${INSTALL_DIR}/.env" "PORT" "${PROXYLESS_PORT}"
echo "📡 Set PORT=${PROXYLESS_PORT} for proxyless mode"
fi
else
echo "📝 Existing ${INSTALL_DIR}/.env found, leaving it unchanged"
if [ "${PROXY}" = "none" ]; then
current_port="$(get_env_value "${INSTALL_DIR}/.env" "PORT" || true)"
if [ -z "${current_port}" ] || [ "${current_port}" = "auto" ]; then
set_env_value "${INSTALL_DIR}/.env" "PORT" "${PROXYLESS_PORT}"
echo "📡 Updated PORT=${PROXYLESS_PORT} for proxyless mode"
fi
fi
fi
install_host_updater
echo ""
echo "Next steps:"
echo " 1. Review ${INSTALL_DIR}/.env"
if [ "${PROXY}" = "caddy" ]; then
echo " 2. Start Synapsis:"
echo " cd ${INSTALL_DIR} && docker compose up -d"
else
echo " 2. Start Synapsis:"
echo " cd ${INSTALL_DIR} && docker compose up -d"
echo " 3. Configure your existing reverse proxy to forward to:"
echo " http://127.0.0.1:${PROXYLESS_PORT}"
echo " (change PORT in ${INSTALL_DIR}/.env if you want a different localhost port)"
fi
-63
View File
@@ -1,63 +0,0 @@
# HTTP to HTTPS redirect (run this first without SSL)
# After SSL certificates are obtained, use the SSL version below
server {
listen 80;
server_name _; # Accept any server name (change to your domain for production)
# Health check endpoint for Docker
location /nginx-health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Proxy all requests to the Next.js app
location / {
proxy_pass http://app: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_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
}
# Static files caching
location /_next/static {
proxy_pass http://app:3000;
proxy_cache_valid 200 365d;
add_header Cache-Control "public, immutable";
}
# API rate limiting
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_conn addr 10;
proxy_pass http://app: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;
}
# Auth endpoints - stricter rate limiting
location /api/auth/ {
limit_req zone=auth burst=5 nodelay;
limit_conn addr 5;
proxy_pass http://app:3000;
proxy_http_version 1.1;
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;
}
}
-91
View File
@@ -1,91 +0,0 @@
# SSL Configuration (use this after obtaining certificates)
# Rename to default.conf after SSL setup
server {
listen 80;
server_name ${DOMAIN};
# Certbot challenge
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Redirect all HTTP to HTTPS
location / {
return 301 https://$server_name$request_uri;
}
}
server {
listen 443 ssl http2;
server_name ${DOMAIN};
# SSL certificates (mounted from certbot)
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS (enable after confirming HTTPS works)
# add_header Strict-Transport-Security "max-age=63072000" always;
# Health check
location /nginx-health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Proxy all requests to the Next.js app
location / {
proxy_pass http://app: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_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
}
# Static files caching
location /_next/static {
proxy_pass http://app:3000;
proxy_cache_valid 200 365d;
add_header Cache-Control "public, immutable";
}
# API rate limiting
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_conn addr 10;
proxy_pass http://app:3000;
proxy_http_version 1.1;
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;
}
# Auth endpoints - stricter rate limiting
location /api/auth/ {
limit_req zone=auth burst=5 nodelay;
limit_conn addr 5;
proxy_pass http://app:3000;
proxy_http_version 1.1;
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;
}
}
-70
View File
@@ -1,70 +0,0 @@
# Nginx configuration for Synapsis
# This configuration includes:
# - Gzip compression
# - Security headers
# - Rate limiting
# - Proper handling of Next.js static files
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging format
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# Performance
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 50M;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/rss+xml
font/truetype
font/opentype
application/vnd.ms-fontobject
image/svg+xml;
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=addr:10m;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Include server configurations
include /etc/nginx/conf.d/*.conf;
}
-127
View File
@@ -1,127 +0,0 @@
#!/bin/sh
set -eu
INSTALL_DIR="${1:-${INSTALL_DIR:-/opt/synapsis}}"
PROJECT_NAME="${PROJECT_NAME:-synapsis}"
IMAGE_REPO="${IMAGE_REPO:-ghcr.io/gnosyslabs/synapsis}"
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "❌ Required command not found: $1" >&2
exit 1
fi
}
confirm_uninstall() {
if [ "${FORCE:-0}" = "1" ] || [ "${YES:-0}" = "1" ]; then
return
fi
echo "========================================"
echo " Synapsis Docker Uninstaller"
echo "========================================"
echo " Install dir: ${INSTALL_DIR}"
echo " Project name: ${PROJECT_NAME}"
echo ""
echo "This will permanently remove:"
echo " - Synapsis containers"
echo " - Synapsis Docker volumes and network"
echo " - Synapsis images pulled from GHCR/local cache"
echo " - ${INSTALL_DIR}"
echo ""
if [ -r /dev/tty ]; then
printf "Type DELETE to continue: " > /dev/tty
read -r confirmation < /dev/tty
else
echo "❌ Interactive confirmation requires a TTY." >&2
echo " Re-run with FORCE=1 if you want to skip the prompt." >&2
exit 1
fi
if [ "$confirmation" != "DELETE" ]; then
echo "Aborted."
exit 1
fi
}
cleanup_with_compose() {
if [ -f "${INSTALL_DIR}/docker-compose.yml" ]; then
echo "🧹 Stopping Synapsis compose stack"
if [ -f "${INSTALL_DIR}/.env" ]; then
docker compose --env-file "${INSTALL_DIR}/.env" -f "${INSTALL_DIR}/docker-compose.yml" down --volumes --remove-orphans || true
else
docker compose -f "${INSTALL_DIR}/docker-compose.yml" down --volumes --remove-orphans || true
fi
fi
}
cleanup_named_resources() {
for container in synapsis-caddy synapsis-app synapsis-db; do
if docker ps -aq --filter "name=^${container}$" | grep -q .; then
echo "🗑️ Removing container ${container}"
docker rm -f "${container}" >/dev/null 2>&1 || true
fi
done
for volume in \
synapsis_postgres_data \
synapsis_uploads_data \
synapsis_port_data \
synapsis_caddy_data \
synapsis_caddy_config
do
if docker volume inspect "${volume}" >/dev/null 2>&1; then
echo "🗑️ Removing volume ${volume}"
docker volume rm -f "${volume}" >/dev/null 2>&1 || true
fi
done
if docker network inspect synapsis_synapsis-network >/dev/null 2>&1; then
echo "🗑️ Removing network synapsis_synapsis-network"
docker network rm synapsis_synapsis-network >/dev/null 2>&1 || true
fi
if command -v systemctl >/dev/null 2>&1; then
systemctl disable --now synapsis-updater.service >/dev/null 2>&1 || true
rm -f /etc/systemd/system/synapsis-updater.service
systemctl daemon-reload >/dev/null 2>&1 || true
fi
rm -rf /var/run/synapsis-updater
}
cleanup_images() {
docker images --format '{{.Repository}}:{{.Tag}}' | while IFS= read -r image; do
case "$image" in
"${IMAGE_REPO}:"*|synapsis:*)
echo "🗑️ Removing image ${image}"
docker rmi -f "$image" >/dev/null 2>&1 || true
;;
esac
done
}
remove_install_dir() {
if [ -d "${INSTALL_DIR}" ]; then
echo "🗑️ Removing ${INSTALL_DIR}"
rm -rf "${INSTALL_DIR}"
fi
}
require_command docker
require_command rm
if [ "$(id -u)" -ne 0 ]; then
echo "❌ Run this uninstaller as root or with sudo." >&2
exit 1
fi
confirm_uninstall
cleanup_with_compose
cleanup_named_resources
cleanup_images
remove_install_dir
echo ""
echo "✅ Synapsis has been removed from this host."
-77
View File
@@ -1,77 +0,0 @@
#!/bin/sh
set -eu
REPO="${REPO:-GnosysLabs/Synapsis}"
REF="${REF:-main}"
INSTALL_DIR="${1:-${INSTALL_DIR:-/opt/synapsis}}"
RAW_BASE="https://raw.githubusercontent.com/${REPO}/${REF}"
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "❌ Required command not found: $1" >&2
exit 1
fi
}
download_file() {
source_path="$1"
target_path="$2"
echo "⬇️ Downloading ${source_path}"
curl -fsSL "${RAW_BASE}/${source_path}" -o "${target_path}"
}
detect_proxy_mode() {
if [ -f "${INSTALL_DIR}/Caddyfile" ] || [ -f "${INSTALL_DIR}/caddy-entrypoint.sh" ]; then
printf 'caddy\n'
return
fi
printf 'none\n'
}
compose_cmd() {
if [ -f "${INSTALL_DIR}/.env" ]; then
docker compose --env-file "${INSTALL_DIR}/.env" -f "${INSTALL_DIR}/docker-compose.yml" "$@"
else
docker compose -f "${INSTALL_DIR}/docker-compose.yml" "$@"
fi
}
require_command curl
require_command docker
require_command chmod
require_command mkdir
if [ ! -d "${INSTALL_DIR}" ]; then
echo "${INSTALL_DIR} does not exist." >&2
exit 1
fi
ACTIVE_PROXY="$(detect_proxy_mode)"
case "${ACTIVE_PROXY}" in
caddy)
download_file "docker-compose.yml" "${INSTALL_DIR}/docker-compose.yml"
download_file "docker/Caddyfile" "${INSTALL_DIR}/Caddyfile"
download_file "docker/caddy-entrypoint.sh" "${INSTALL_DIR}/caddy-entrypoint.sh"
chmod 755 "${INSTALL_DIR}/caddy-entrypoint.sh"
;;
none)
download_file "docker-compose.proxyless.yml" "${INSTALL_DIR}/docker-compose.yml"
rm -f "${INSTALL_DIR}/Caddyfile" "${INSTALL_DIR}/caddy-entrypoint.sh"
;;
esac
download_file "docker/host-updater.py" "${INSTALL_DIR}/host-updater.py"
download_file "docker/update-local.sh" "${INSTALL_DIR}/update-local.sh"
chmod 755 "${INSTALL_DIR}/host-updater.py" "${INSTALL_DIR}/update-local.sh"
echo "🐳 Pulling latest Synapsis image"
compose_cmd pull
echo "🚀 Restarting Synapsis"
compose_cmd up -d --remove-orphans
echo ""
echo "✅ Synapsis has been updated to the latest published image."
-237
View File
@@ -1,237 +0,0 @@
#!/bin/sh
set -eu
REPO="${REPO:-GnosysLabs/Synapsis}"
REF="${REF:-main}"
INSTALL_DIR="${1:-${INSTALL_DIR:-/opt/synapsis}}"
RAW_BASE="https://raw.githubusercontent.com/${REPO}/${REF}"
PROXY="${PROXY:-}"
HOST_UPDATER_SOCKET_PATH="${HOST_UPDATER_SOCKET_PATH:-/var/run/synapsis-updater/updater.sock}"
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "❌ Required command not found: $1" >&2
exit 1
fi
}
download_file() {
source_path="$1"
target_path="$2"
echo "⬇️ Downloading ${source_path}"
curl -fsSL "${RAW_BASE}/${source_path}" -o "${target_path}"
}
set_env_value() {
file="$1"
key="$2"
value="$3"
escaped_value=$(printf '%s\n' "$value" | sed -e 's/[\/&]/\\&/g')
sed -i.bak -E "s|^${key}=.*$|${key}=${escaped_value}|" "$file"
rm -f "${file}.bak"
}
ensure_env_value() {
file="$1"
key="$2"
value="$3"
if grep -q "^${key}=" "$file" 2>/dev/null; then
set_env_value "$file" "$key" "$value"
return
fi
printf '%s=%s\n' "$key" "$value" >> "$file"
}
get_env_value() {
file="$1"
key="$2"
sed -n -E "s/^${key}=(.*)$/\\1/p" "$file" | head -n 1
}
generate_token() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
return
fi
date +%s | sha256sum | awk '{print $1}'
}
normalize_proxy_mode() {
case "$1" in
caddy|none)
printf '%s\n' "$1"
;;
*)
echo "❌ Unsupported PROXY mode: $1" >&2
echo " Supported values: caddy, none" >&2
exit 1
;;
esac
}
detect_proxy_mode() {
if [ -n "${PROXY}" ]; then
normalize_proxy_mode "${PROXY}"
return
fi
if [ -f "${INSTALL_DIR}/Caddyfile" ] || [ -f "${INSTALL_DIR}/caddy-entrypoint.sh" ]; then
printf 'caddy\n'
return
fi
printf 'none\n'
}
install_python3_if_needed() {
if command -v python3 >/dev/null 2>&1; then
return
fi
if command -v apt-get >/dev/null 2>&1; then
apt-get update
apt-get install -y python3
return
fi
if command -v apk >/dev/null 2>&1; then
apk add --no-cache python3
return
fi
}
install_host_updater() {
if [ "$(id -u)" -ne 0 ]; then
return
fi
if [ ! -f "${INSTALL_DIR}/.env" ]; then
return
fi
download_file "docker/host-updater.py" "${INSTALL_DIR}/host-updater.py"
download_file "docker/update-local.sh" "${INSTALL_DIR}/update-local.sh"
chmod 755 "${INSTALL_DIR}/host-updater.py" "${INSTALL_DIR}/update-local.sh"
mkdir -p "$(dirname "${HOST_UPDATER_SOCKET_PATH}")"
updater_token="$(get_env_value "${INSTALL_DIR}/.env" "HOST_UPDATER_TOKEN" || true)"
if [ -z "${updater_token}" ]; then
updater_token="$(generate_token)"
ensure_env_value "${INSTALL_DIR}/.env" "HOST_UPDATER_TOKEN" "${updater_token}"
fi
ensure_env_value "${INSTALL_DIR}/.env" "HOST_UPDATER_SOCKET" "${HOST_UPDATER_SOCKET_PATH}"
if ! command -v systemctl >/dev/null 2>&1; then
return
fi
install_python3_if_needed
if ! command -v python3 >/dev/null 2>&1; then
return
fi
cat > "${INSTALL_DIR}/updater.env" <<EOF
HOST_UPDATER_TOKEN=${updater_token}
REPO=${REPO}
REF=${REF}
EOF
chmod 600 "${INSTALL_DIR}/updater.env"
cat > /etc/systemd/system/synapsis-updater.service <<EOF
[Unit]
Description=Synapsis Host Updater
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=${INSTALL_DIR}
Environment=INSTALL_DIR=${INSTALL_DIR}
Environment=HOST_UPDATER_SOCKET=${HOST_UPDATER_SOCKET_PATH}
Environment=HOST_UPDATER_STATUS_FILE=${INSTALL_DIR}/updater-status.json
Environment=HOST_UPDATER_LOG_FILE=${INSTALL_DIR}/updater.log
Environment=HOST_UPDATER_SCRIPT=${INSTALL_DIR}/update-local.sh
EnvironmentFile=${INSTALL_DIR}/updater.env
ExecStart=/usr/bin/env python3 ${INSTALL_DIR}/host-updater.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now synapsis-updater.service >/dev/null 2>&1 || true
systemctl restart synapsis-updater.service >/dev/null 2>&1 || true
}
require_command curl
require_command docker
require_command chmod
require_command mkdir
if [ ! -d "${INSTALL_DIR}" ]; then
echo "${INSTALL_DIR} does not exist." >&2
echo " Run the installer first:" >&2
echo " curl -fsSL https://synapsis.social/install.sh | bash" >&2
exit 1
fi
if [ ! -f "${INSTALL_DIR}/docker-compose.yml" ]; then
echo "${INSTALL_DIR}/docker-compose.yml was not found." >&2
echo " This does not look like a Synapsis install directory." >&2
exit 1
fi
ACTIVE_PROXY="$(detect_proxy_mode)"
echo "========================================"
echo " Synapsis Docker Updater"
echo "========================================"
echo " Repo: ${REPO}"
echo " Ref: ${REF}"
echo " Install dir: ${INSTALL_DIR}"
echo " Proxy mode: ${ACTIVE_PROXY}"
echo "========================================"
mkdir -p "${INSTALL_DIR}"
download_file "docker/.env.example" "${INSTALL_DIR}/.env.example"
case "${ACTIVE_PROXY}" in
caddy)
download_file "docker-compose.yml" "${INSTALL_DIR}/docker-compose.yml"
download_file "docker/Caddyfile" "${INSTALL_DIR}/Caddyfile"
download_file "docker/caddy-entrypoint.sh" "${INSTALL_DIR}/caddy-entrypoint.sh"
chmod 755 "${INSTALL_DIR}/caddy-entrypoint.sh"
;;
none)
download_file "docker-compose.proxyless.yml" "${INSTALL_DIR}/docker-compose.yml"
rm -f "${INSTALL_DIR}/Caddyfile" "${INSTALL_DIR}/caddy-entrypoint.sh"
;;
esac
install_host_updater
echo "🐳 Pulling latest Synapsis image"
if [ -f "${INSTALL_DIR}/.env" ]; then
docker compose --env-file "${INSTALL_DIR}/.env" -f "${INSTALL_DIR}/docker-compose.yml" pull
echo "🚀 Restarting Synapsis"
docker compose --env-file "${INSTALL_DIR}/.env" -f "${INSTALL_DIR}/docker-compose.yml" up -d --remove-orphans
else
docker compose -f "${INSTALL_DIR}/docker-compose.yml" pull
echo "🚀 Restarting Synapsis"
docker compose -f "${INSTALL_DIR}/docker-compose.yml" up -d --remove-orphans
fi
echo ""
echo "✅ Synapsis has been updated to the latest published image."
+2 -2
View File
@@ -7,8 +7,8 @@ dotenv.config();
export default defineConfig({ export default defineConfig({
schema: './src/db/schema.ts', schema: './src/db/schema.ts',
out: './drizzle', out: './drizzle',
dialect: 'postgresql', dialect: 'sqlite',
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL!, url: process.env.DATABASE_PATH || './data/synapsis.db',
}, },
}); });
-369
View File
@@ -1,369 +0,0 @@
CREATE TABLE "blocks" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"blocked_user_id" uuid NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "bot_activity_logs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"bot_id" uuid NOT NULL,
"action" text NOT NULL,
"details" text NOT NULL,
"success" boolean NOT NULL,
"error_message" text,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "bot_content_items" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"source_id" uuid NOT NULL,
"external_id" text NOT NULL,
"title" text NOT NULL,
"content" text,
"url" text NOT NULL,
"published_at" timestamp NOT NULL,
"fetched_at" timestamp DEFAULT now() NOT NULL,
"is_processed" boolean DEFAULT false NOT NULL,
"processed_at" timestamp,
"post_id" uuid,
"interest_score" integer,
"interest_reason" text
);
--> statement-breakpoint
CREATE TABLE "bot_content_sources" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"bot_id" uuid NOT NULL,
"type" text NOT NULL,
"url" text NOT NULL,
"subreddit" text,
"api_key_encrypted" text,
"fetch_interval_minutes" integer DEFAULT 30 NOT NULL,
"keywords" text,
"is_active" boolean DEFAULT true NOT NULL,
"last_fetch_at" timestamp,
"last_error" text,
"consecutive_errors" integer DEFAULT 0 NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "bot_mentions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"bot_id" uuid NOT NULL,
"post_id" uuid NOT NULL,
"author_id" uuid NOT NULL,
"content" text NOT NULL,
"is_processed" boolean DEFAULT false NOT NULL,
"processed_at" timestamp,
"response_post_id" uuid,
"is_remote" boolean DEFAULT false NOT NULL,
"remote_actor_url" text,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "bot_rate_limits" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"bot_id" uuid NOT NULL,
"window_start" timestamp NOT NULL,
"window_type" text NOT NULL,
"post_count" integer DEFAULT 0 NOT NULL,
"reply_count" integer DEFAULT 0 NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "bots" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"name" text NOT NULL,
"handle" text NOT NULL,
"bio" text,
"avatar_url" text,
"personality_config" text NOT NULL,
"llm_provider" text NOT NULL,
"llm_model" text NOT NULL,
"llm_api_key_encrypted" text NOT NULL,
"schedule_config" text,
"autonomous_mode" boolean DEFAULT false NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"is_suspended" boolean DEFAULT false NOT NULL,
"suspension_reason" text,
"suspended_at" timestamp,
"public_key" text NOT NULL,
"private_key_encrypted" text NOT NULL,
"last_post_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "bots_handle_unique" UNIQUE("handle")
);
--> statement-breakpoint
CREATE TABLE "follows" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"follower_id" uuid NOT NULL,
"following_id" uuid NOT NULL,
"ap_id" text,
"pending" boolean DEFAULT false,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "follows_ap_id_unique" UNIQUE("ap_id")
);
--> statement-breakpoint
CREATE TABLE "handle_registry" (
"handle" text PRIMARY KEY NOT NULL,
"did" text NOT NULL,
"node_domain" text NOT NULL,
"registered_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "likes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"post_id" uuid NOT NULL,
"ap_id" text,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "likes_ap_id_unique" UNIQUE("ap_id")
);
--> statement-breakpoint
CREATE TABLE "media" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"post_id" uuid,
"url" text NOT NULL,
"alt_text" text,
"mime_type" text,
"width" integer,
"height" integer,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "mutes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"muted_user_id" uuid NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "nodes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"domain" text NOT NULL,
"name" text NOT NULL,
"description" text,
"long_description" text,
"rules" text,
"banner_url" text,
"accent_color" text DEFAULT '#FFFFFF',
"public_key" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "nodes_domain_unique" UNIQUE("domain")
);
--> statement-breakpoint
CREATE TABLE "notifications" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"actor_id" uuid NOT NULL,
"post_id" uuid,
"type" text NOT NULL,
"read_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "posts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"content" text NOT NULL,
"reply_to_id" uuid,
"repost_of_id" uuid,
"likes_count" integer DEFAULT 0 NOT NULL,
"reposts_count" integer DEFAULT 0 NOT NULL,
"replies_count" integer DEFAULT 0 NOT NULL,
"is_removed" boolean DEFAULT false NOT NULL,
"removed_at" timestamp,
"removed_by" uuid,
"removed_reason" text,
"ap_id" text,
"ap_url" text,
"link_preview_url" text,
"link_preview_title" text,
"link_preview_description" text,
"link_preview_image" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "posts_ap_id_unique" UNIQUE("ap_id")
);
--> statement-breakpoint
CREATE TABLE "remote_followers" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"actor_url" text NOT NULL,
"inbox_url" text NOT NULL,
"shared_inbox_url" text,
"handle" text,
"activity_id" text,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "remote_followers_actor_url_unique" UNIQUE("actor_url")
);
--> statement-breakpoint
CREATE TABLE "remote_follows" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"follower_id" uuid NOT NULL,
"target_handle" text NOT NULL,
"target_actor_url" text NOT NULL,
"inbox_url" text NOT NULL,
"activity_id" text NOT NULL,
"display_name" text,
"bio" text,
"avatar_url" text,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "remote_posts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"ap_id" text NOT NULL,
"author_handle" text NOT NULL,
"author_actor_url" text NOT NULL,
"author_display_name" text,
"author_avatar_url" text,
"content" text NOT NULL,
"published_at" timestamp NOT NULL,
"link_preview_url" text,
"link_preview_title" text,
"link_preview_description" text,
"link_preview_image" text,
"media_json" text,
"fetched_at" timestamp DEFAULT now() NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "remote_posts_ap_id_unique" UNIQUE("ap_id")
);
--> statement-breakpoint
CREATE TABLE "reports" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"reporter_id" uuid,
"target_type" text NOT NULL,
"target_id" uuid NOT NULL,
"reason" text NOT NULL,
"status" text DEFAULT 'open' NOT NULL,
"resolved_at" timestamp,
"resolved_by" uuid,
"resolution_note" text,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "sessions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"token" text NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "sessions_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"did" text NOT NULL,
"handle" text NOT NULL,
"email" text,
"password_hash" text,
"display_name" text,
"bio" text,
"avatar_url" text,
"header_url" text,
"private_key_encrypted" text,
"public_key" text NOT NULL,
"node_id" uuid,
"is_suspended" boolean DEFAULT false NOT NULL,
"suspension_reason" text,
"suspended_at" timestamp,
"is_silenced" boolean DEFAULT false NOT NULL,
"silence_reason" text,
"silenced_at" timestamp,
"moved_to" text,
"moved_from" text,
"migrated_at" timestamp,
"followers_count" integer DEFAULT 0 NOT NULL,
"following_count" integer DEFAULT 0 NOT NULL,
"posts_count" integer DEFAULT 0 NOT NULL,
"website" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "users_did_unique" UNIQUE("did"),
CONSTRAINT "users_handle_unique" UNIQUE("handle"),
CONSTRAINT "users_email_unique" UNIQUE("email")
);
--> statement-breakpoint
ALTER TABLE "blocks" ADD CONSTRAINT "blocks_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "blocks" ADD CONSTRAINT "blocks_blocked_user_id_users_id_fk" FOREIGN KEY ("blocked_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "bot_activity_logs" ADD CONSTRAINT "bot_activity_logs_bot_id_bots_id_fk" FOREIGN KEY ("bot_id") REFERENCES "public"."bots"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "bot_content_items" ADD CONSTRAINT "bot_content_items_source_id_bot_content_sources_id_fk" FOREIGN KEY ("source_id") REFERENCES "public"."bot_content_sources"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "bot_content_items" ADD CONSTRAINT "bot_content_items_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "bot_content_sources" ADD CONSTRAINT "bot_content_sources_bot_id_bots_id_fk" FOREIGN KEY ("bot_id") REFERENCES "public"."bots"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "bot_mentions" ADD CONSTRAINT "bot_mentions_bot_id_bots_id_fk" FOREIGN KEY ("bot_id") REFERENCES "public"."bots"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "bot_mentions" ADD CONSTRAINT "bot_mentions_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "bot_mentions" ADD CONSTRAINT "bot_mentions_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "bot_mentions" ADD CONSTRAINT "bot_mentions_response_post_id_posts_id_fk" FOREIGN KEY ("response_post_id") REFERENCES "public"."posts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "bot_rate_limits" ADD CONSTRAINT "bot_rate_limits_bot_id_bots_id_fk" FOREIGN KEY ("bot_id") REFERENCES "public"."bots"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "bots" ADD CONSTRAINT "bots_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "follows" ADD CONSTRAINT "follows_follower_id_users_id_fk" FOREIGN KEY ("follower_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "follows" ADD CONSTRAINT "follows_following_id_users_id_fk" FOREIGN KEY ("following_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "likes" ADD CONSTRAINT "likes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "likes" ADD CONSTRAINT "likes_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "media" ADD CONSTRAINT "media_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "media" ADD CONSTRAINT "media_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "mutes" ADD CONSTRAINT "mutes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "mutes" ADD CONSTRAINT "mutes_muted_user_id_users_id_fk" FOREIGN KEY ("muted_user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_actor_id_users_id_fk" FOREIGN KEY ("actor_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "posts" ADD CONSTRAINT "posts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "posts" ADD CONSTRAINT "posts_removed_by_users_id_fk" FOREIGN KEY ("removed_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "remote_followers" ADD CONSTRAINT "remote_followers_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "remote_follows" ADD CONSTRAINT "remote_follows_follower_id_users_id_fk" FOREIGN KEY ("follower_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "reports" ADD CONSTRAINT "reports_reporter_id_users_id_fk" FOREIGN KEY ("reporter_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "reports" ADD CONSTRAINT "reports_resolved_by_users_id_fk" FOREIGN KEY ("resolved_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "users" ADD CONSTRAINT "users_node_id_nodes_id_fk" FOREIGN KEY ("node_id") REFERENCES "public"."nodes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "blocks_user_idx" ON "blocks" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "bot_activity_logs_bot_idx" ON "bot_activity_logs" USING btree ("bot_id");--> statement-breakpoint
CREATE INDEX "bot_activity_logs_action_idx" ON "bot_activity_logs" USING btree ("action");--> statement-breakpoint
CREATE INDEX "bot_activity_logs_created_idx" ON "bot_activity_logs" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "bot_content_items_source_idx" ON "bot_content_items" USING btree ("source_id");--> statement-breakpoint
CREATE INDEX "bot_content_items_processed_idx" ON "bot_content_items" USING btree ("is_processed");--> statement-breakpoint
CREATE INDEX "bot_content_items_external_idx" ON "bot_content_items" USING btree ("external_id");--> statement-breakpoint
CREATE INDEX "bot_content_sources_bot_idx" ON "bot_content_sources" USING btree ("bot_id");--> statement-breakpoint
CREATE INDEX "bot_content_sources_type_idx" ON "bot_content_sources" USING btree ("type");--> statement-breakpoint
CREATE INDEX "bot_mentions_bot_idx" ON "bot_mentions" USING btree ("bot_id");--> statement-breakpoint
CREATE INDEX "bot_mentions_processed_idx" ON "bot_mentions" USING btree ("is_processed");--> statement-breakpoint
CREATE INDEX "bot_mentions_created_idx" ON "bot_mentions" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "bot_rate_limits_bot_window_idx" ON "bot_rate_limits" USING btree ("bot_id","window_start");--> statement-breakpoint
CREATE INDEX "bots_user_id_idx" ON "bots" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "bots_handle_idx" ON "bots" USING btree ("handle");--> statement-breakpoint
CREATE INDEX "bots_active_idx" ON "bots" USING btree ("is_active");--> statement-breakpoint
CREATE INDEX "follows_follower_idx" ON "follows" USING btree ("follower_id");--> statement-breakpoint
CREATE INDEX "follows_following_idx" ON "follows" USING btree ("following_id");--> statement-breakpoint
CREATE INDEX "handle_registry_updated_idx" ON "handle_registry" USING btree ("updated_at");--> statement-breakpoint
CREATE INDEX "likes_user_post_idx" ON "likes" USING btree ("user_id","post_id");--> statement-breakpoint
CREATE INDEX "media_user_idx" ON "media" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "media_post_idx" ON "media" USING btree ("post_id");--> statement-breakpoint
CREATE INDEX "mutes_user_idx" ON "mutes" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "notifications_user_idx" ON "notifications" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "notifications_created_idx" ON "notifications" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "posts_user_id_idx" ON "posts" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "posts_created_at_idx" ON "posts" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "posts_reply_to_idx" ON "posts" USING btree ("reply_to_id");--> statement-breakpoint
CREATE INDEX "posts_removed_idx" ON "posts" USING btree ("is_removed");--> statement-breakpoint
CREATE INDEX "remote_followers_user_idx" ON "remote_followers" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "remote_followers_actor_idx" ON "remote_followers" USING btree ("actor_url");--> statement-breakpoint
CREATE INDEX "remote_follows_follower_idx" ON "remote_follows" USING btree ("follower_id");--> statement-breakpoint
CREATE INDEX "remote_follows_target_idx" ON "remote_follows" USING btree ("target_handle");--> statement-breakpoint
CREATE INDEX "remote_posts_author_idx" ON "remote_posts" USING btree ("author_handle");--> statement-breakpoint
CREATE INDEX "remote_posts_published_idx" ON "remote_posts" USING btree ("published_at");--> statement-breakpoint
CREATE INDEX "remote_posts_ap_id_idx" ON "remote_posts" USING btree ("ap_id");--> statement-breakpoint
CREATE INDEX "reports_status_idx" ON "reports" USING btree ("status");--> statement-breakpoint
CREATE INDEX "reports_target_idx" ON "reports" USING btree ("target_type","target_id");--> statement-breakpoint
CREATE INDEX "reports_reporter_idx" ON "reports" USING btree ("reporter_id");--> statement-breakpoint
CREATE INDEX "sessions_token_idx" ON "sessions" USING btree ("token");--> statement-breakpoint
CREATE INDEX "sessions_user_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "users_handle_idx" ON "users" USING btree ("handle");--> statement-breakpoint
CREATE INDEX "users_did_idx" ON "users" USING btree ("did");--> statement-breakpoint
CREATE INDEX "users_suspended_idx" ON "users" USING btree ("is_suspended");--> statement-breakpoint
CREATE INDEX "users_silenced_idx" ON "users" USING btree ("is_silenced");
-2
View File
@@ -1,2 +0,0 @@
-- Remove fetch_interval_minutes column from bot_content_sources
ALTER TABLE "bot_content_sources" DROP COLUMN IF EXISTS "fetch_interval_minutes";
-5
View File
@@ -1,5 +0,0 @@
-- Add bot_id column to posts table
ALTER TABLE "posts" ADD COLUMN "bot_id" uuid REFERENCES "bots"("id") ON DELETE SET NULL;
-- Create index for bot_id
CREATE INDEX IF NOT EXISTS "posts_bot_id_idx" ON "posts" ("bot_id");
-24
View File
@@ -1,24 +0,0 @@
ALTER TABLE "bots" DROP CONSTRAINT "bots_handle_unique";--> statement-breakpoint
ALTER TABLE "bot_content_items" DROP CONSTRAINT "bot_content_items_post_id_posts_id_fk";
--> statement-breakpoint
DROP INDEX "bots_handle_idx";--> statement-breakpoint
ALTER TABLE "bot_content_sources" ADD COLUMN "source_config" text;--> statement-breakpoint
ALTER TABLE "bots" ADD COLUMN "owner_id" uuid NOT NULL;--> statement-breakpoint
ALTER TABLE "nodes" ADD COLUMN "logo_url" text;--> statement-breakpoint
ALTER TABLE "posts" ADD COLUMN "bot_id" uuid;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "is_bot" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "bot_owner_id" uuid;--> statement-breakpoint
ALTER TABLE "bot_content_items" ADD CONSTRAINT "bot_content_items_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "bots" ADD CONSTRAINT "bots_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "posts" ADD CONSTRAINT "posts_bot_id_bots_id_fk" FOREIGN KEY ("bot_id") REFERENCES "public"."bots"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "users" ADD CONSTRAINT "users_bot_owner_id_users_id_fk" FOREIGN KEY ("bot_owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "bots_owner_id_idx" ON "bots" USING btree ("owner_id");--> statement-breakpoint
CREATE INDEX "posts_bot_id_idx" ON "posts" USING btree ("bot_id");--> statement-breakpoint
CREATE INDEX "users_is_bot_idx" ON "users" USING btree ("is_bot");--> statement-breakpoint
CREATE INDEX "users_bot_owner_idx" ON "users" USING btree ("bot_owner_id");--> statement-breakpoint
ALTER TABLE "bot_content_sources" DROP COLUMN "fetch_interval_minutes";--> statement-breakpoint
ALTER TABLE "bots" DROP COLUMN "handle";--> statement-breakpoint
ALTER TABLE "bots" DROP COLUMN "bio";--> statement-breakpoint
ALTER TABLE "bots" DROP COLUMN "avatar_url";--> statement-breakpoint
ALTER TABLE "bots" DROP COLUMN "public_key";--> statement-breakpoint
ALTER TABLE "bots" DROP COLUMN "private_key_encrypted";
@@ -1,4 +0,0 @@
-- Fix bot_content_items post_id foreign key to allow cascade on delete
ALTER TABLE "bot_content_items" DROP CONSTRAINT IF EXISTS "bot_content_items_post_id_posts_id_fk";
ALTER TABLE "bot_content_items" ADD CONSTRAINT "bot_content_items_post_id_posts_id_fk"
FOREIGN KEY ("post_id") REFERENCES "posts"("id") ON DELETE SET NULL;
-56
View File
@@ -1,56 +0,0 @@
CREATE TABLE "swarm_nodes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"domain" text NOT NULL,
"name" text,
"description" text,
"logo_url" text,
"public_key" text,
"software_version" text,
"user_count" integer,
"post_count" integer,
"discovered_via" text,
"discovered_at" timestamp DEFAULT now() NOT NULL,
"last_seen_at" timestamp DEFAULT now() NOT NULL,
"last_sync_at" timestamp,
"consecutive_failures" integer DEFAULT 0 NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"trust_score" integer DEFAULT 50 NOT NULL,
"capabilities" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "swarm_nodes_domain_unique" UNIQUE("domain")
);
--> statement-breakpoint
CREATE TABLE "swarm_seeds" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"domain" text NOT NULL,
"priority" integer DEFAULT 100 NOT NULL,
"is_enabled" boolean DEFAULT true NOT NULL,
"last_contact_at" timestamp,
"consecutive_failures" integer DEFAULT 0 NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "swarm_seeds_domain_unique" UNIQUE("domain")
);
--> statement-breakpoint
CREATE TABLE "swarm_sync_log" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"remote_domain" text NOT NULL,
"direction" text NOT NULL,
"nodes_received" integer DEFAULT 0 NOT NULL,
"nodes_sent" integer DEFAULT 0 NOT NULL,
"handles_received" integer DEFAULT 0 NOT NULL,
"handles_sent" integer DEFAULT 0 NOT NULL,
"success" boolean NOT NULL,
"error_message" text,
"duration_ms" integer,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE INDEX "swarm_nodes_domain_idx" ON "swarm_nodes" USING btree ("domain");--> statement-breakpoint
CREATE INDEX "swarm_nodes_active_idx" ON "swarm_nodes" USING btree ("is_active");--> statement-breakpoint
CREATE INDEX "swarm_nodes_last_seen_idx" ON "swarm_nodes" USING btree ("last_seen_at");--> statement-breakpoint
CREATE INDEX "swarm_nodes_trust_idx" ON "swarm_nodes" USING btree ("trust_score");--> statement-breakpoint
CREATE INDEX "swarm_seeds_enabled_idx" ON "swarm_seeds" USING btree ("is_enabled");--> statement-breakpoint
CREATE INDEX "swarm_seeds_priority_idx" ON "swarm_seeds" USING btree ("priority");--> statement-breakpoint
CREATE INDEX "swarm_sync_log_remote_idx" ON "swarm_sync_log" USING btree ("remote_domain");--> statement-breakpoint
CREATE INDEX "swarm_sync_log_created_idx" ON "swarm_sync_log" USING btree ("created_at");
-2
View File
@@ -1,2 +0,0 @@
-- Add source_config column for Brave News and News API query builder configurations
ALTER TABLE "bot_content_sources" ADD COLUMN "source_config" text;
-21
View File
@@ -1,21 +0,0 @@
CREATE TABLE "muted_nodes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"node_domain" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "nodes" ADD COLUMN "is_nsfw" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "posts" ADD COLUMN "is_nsfw" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "swarm_nodes" ADD COLUMN "is_nsfw" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "is_nsfw" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "nsfw_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "age_verified_at" timestamp;--> statement-breakpoint
ALTER TABLE "muted_nodes" ADD CONSTRAINT "muted_nodes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "muted_nodes_user_idx" ON "muted_nodes" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "muted_nodes_domain_idx" ON "muted_nodes" USING btree ("node_domain");--> statement-breakpoint
CREATE INDEX "blocks_blocked_user_idx" ON "blocks" USING btree ("blocked_user_id");--> statement-breakpoint
CREATE INDEX "mutes_muted_user_idx" ON "mutes" USING btree ("muted_user_id");--> statement-breakpoint
CREATE INDEX "posts_nsfw_idx" ON "posts" USING btree ("is_nsfw");--> statement-breakpoint
CREATE INDEX "swarm_nodes_nsfw_idx" ON "swarm_nodes" USING btree ("is_nsfw");--> statement-breakpoint
CREATE INDEX "users_nsfw_idx" ON "users" USING btree ("is_nsfw");
-34
View File
@@ -1,34 +0,0 @@
-- Bots as First-Class Users Migration
-- This migration transforms bots to have their own user accounts
-- Add bot-related fields to users table
ALTER TABLE "users" ADD COLUMN "is_bot" boolean DEFAULT false NOT NULL;
ALTER TABLE "users" ADD COLUMN "bot_owner_id" uuid REFERENCES "users"("id") ON DELETE CASCADE;
-- Create indexes for bot fields
CREATE INDEX IF NOT EXISTS "users_is_bot_idx" ON "users" ("is_bot");
CREATE INDEX IF NOT EXISTS "users_bot_owner_idx" ON "users" ("bot_owner_id");
-- Add owner_id to bots table (will be populated during migration)
ALTER TABLE "bots" ADD COLUMN "owner_id" uuid REFERENCES "users"("id") ON DELETE CASCADE;
-- Copy existing userId to ownerId (existing bots were owned by the user)
UPDATE "bots" SET "owner_id" = "user_id";
-- Make owner_id NOT NULL after populating
ALTER TABLE "bots" ALTER COLUMN "owner_id" SET NOT NULL;
-- Create index for owner_id
CREATE INDEX IF NOT EXISTS "bots_owner_id_idx" ON "bots" ("owner_id");
-- Remove columns that are now on the user account
-- Note: handle, bio, avatarUrl, publicKey, privateKeyEncrypted move to users table
-- We'll keep them for now and handle migration in application code
-- ALTER TABLE "bots" DROP COLUMN "handle";
-- ALTER TABLE "bots" DROP COLUMN "bio";
-- ALTER TABLE "bots" DROP COLUMN "avatar_url";
-- ALTER TABLE "bots" DROP COLUMN "public_key";
-- ALTER TABLE "bots" DROP COLUMN "private_key_encrypted";
-- Drop the handle index since handle is now on users
DROP INDEX IF EXISTS "bots_handle_idx";
-3
View File
@@ -1,3 +0,0 @@
ALTER TABLE "remote_followers" DROP CONSTRAINT "remote_followers_actor_url_unique";--> statement-breakpoint
ALTER TABLE "nodes" ADD COLUMN "favicon_url" text;--> statement-breakpoint
CREATE UNIQUE INDEX "remote_followers_user_actor_unique" ON "remote_followers" USING btree ("user_id","actor_url");
-2
View File
@@ -1,2 +0,0 @@
-- Add favicon_url column to nodes table
ALTER TABLE "nodes" ADD COLUMN IF NOT EXISTS "favicon_url" text;
-29
View File
@@ -1,29 +0,0 @@
CREATE TABLE "remote_likes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"post_id" uuid NOT NULL,
"actor_handle" text NOT NULL,
"actor_node_domain" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "remote_reposts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"post_id" uuid NOT NULL,
"actor_handle" text NOT NULL,
"actor_node_domain" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "nodes" ADD COLUMN "turnstile_site_key" text;--> statement-breakpoint
ALTER TABLE "nodes" ADD COLUMN "turnstile_secret_key" text;--> statement-breakpoint
ALTER TABLE "posts" ADD COLUMN "swarm_reply_to_id" text;--> statement-breakpoint
ALTER TABLE "posts" ADD COLUMN "swarm_reply_to_content" text;--> statement-breakpoint
ALTER TABLE "posts" ADD COLUMN "swarm_reply_to_author" text;--> statement-breakpoint
ALTER TABLE "remote_likes" ADD CONSTRAINT "remote_likes_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "remote_reposts" ADD CONSTRAINT "remote_reposts_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "remote_likes_post_idx" ON "remote_likes" USING btree ("post_id");--> statement-breakpoint
CREATE INDEX "remote_likes_actor_idx" ON "remote_likes" USING btree ("actor_handle","actor_node_domain");--> statement-breakpoint
CREATE UNIQUE INDEX "remote_likes_unique" ON "remote_likes" USING btree ("post_id","actor_handle","actor_node_domain");--> statement-breakpoint
CREATE INDEX "remote_reposts_post_idx" ON "remote_reposts" USING btree ("post_id");--> statement-breakpoint
CREATE INDEX "remote_reposts_actor_idx" ON "remote_reposts" USING btree ("actor_handle","actor_node_domain");--> statement-breakpoint
CREATE UNIQUE INDEX "remote_reposts_unique" ON "remote_reposts" USING btree ("post_id","actor_handle","actor_node_domain");
-46
View File
@@ -1,46 +0,0 @@
CREATE TABLE "chat_conversations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"type" text DEFAULT 'direct' NOT NULL,
"participant1_id" uuid NOT NULL,
"participant2_handle" text NOT NULL,
"last_message_at" timestamp,
"last_message_preview" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "chat_messages" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"conversation_id" uuid NOT NULL,
"sender_handle" text NOT NULL,
"sender_display_name" text,
"sender_avatar_url" text,
"sender_node_domain" text,
"encrypted_content" text NOT NULL,
"swarm_message_id" text,
"delivered_at" timestamp,
"read_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "chat_messages_swarm_message_id_unique" UNIQUE("swarm_message_id")
);
--> statement-breakpoint
CREATE TABLE "chat_typing_indicators" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"conversation_id" uuid NOT NULL,
"user_handle" text NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "chat_conversations" ADD CONSTRAINT "chat_conversations_participant1_id_users_id_fk" FOREIGN KEY ("participant1_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_conversation_id_chat_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."chat_conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "chat_typing_indicators" ADD CONSTRAINT "chat_typing_indicators_conversation_id_chat_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."chat_conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "chat_conversations_participant1_idx" ON "chat_conversations" USING btree ("participant1_id");--> statement-breakpoint
CREATE INDEX "chat_conversations_last_message_idx" ON "chat_conversations" USING btree ("last_message_at");--> statement-breakpoint
CREATE UNIQUE INDEX "chat_conversations_unique" ON "chat_conversations" USING btree ("participant1_id","participant2_handle");--> statement-breakpoint
CREATE INDEX "chat_messages_conversation_idx" ON "chat_messages" USING btree ("conversation_id");--> statement-breakpoint
CREATE INDEX "chat_messages_created_idx" ON "chat_messages" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "chat_messages_swarm_id_idx" ON "chat_messages" USING btree ("swarm_message_id");--> statement-breakpoint
CREATE INDEX "chat_typing_conversation_idx" ON "chat_typing_indicators" USING btree ("conversation_id");--> statement-breakpoint
CREATE INDEX "chat_typing_expires_idx" ON "chat_typing_indicators" USING btree ("expires_at");--> statement-breakpoint
CREATE UNIQUE INDEX "chat_typing_unique" ON "chat_typing_indicators" USING btree ("conversation_id","user_handle");
-2
View File
@@ -1,2 +0,0 @@
-- Add private_key_encrypted column to nodes table for cryptographic signing
ALTER TABLE "nodes" ADD COLUMN "private_key_encrypted" text;
-21
View File
@@ -1,21 +0,0 @@
CREATE TABLE "remote_identity_cache" (
"did" text PRIMARY KEY NOT NULL,
"public_key" text NOT NULL,
"fetched_at" timestamp NOT NULL,
"expires_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "signed_action_dedupe" (
"action_id" text PRIMARY KEY NOT NULL,
"did" text NOT NULL,
"nonce" text NOT NULL,
"ts" bigint NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "chat_messages" ADD COLUMN "sender_encrypted_content" text;--> statement-breakpoint
ALTER TABLE "chat_messages" ADD COLUMN "sender_chat_public_key" text;--> statement-breakpoint
ALTER TABLE "nodes" ADD COLUMN "private_key_encrypted" text;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "chat_public_key" text;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "chat_private_key_encrypted" text;--> statement-breakpoint
CREATE INDEX "signed_action_dedupe_created_idx" ON "signed_action_dedupe" USING btree ("created_at");
@@ -1,43 +0,0 @@
CREATE TABLE "chat_device_bundles" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"did" text NOT NULL,
"device_id" text NOT NULL,
"identity_key" text NOT NULL,
"signed_pre_key" text NOT NULL,
"signature" text NOT NULL,
"last_seen_at" timestamp DEFAULT now() NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "chat_inbox" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"recipient_did" text NOT NULL,
"recipient_device_id" text,
"sender_did" text NOT NULL,
"envelope_json" text NOT NULL,
"is_read" boolean DEFAULT false NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"expires_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "chat_one_time_keys" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"bundle_id" uuid NOT NULL,
"key_id" integer NOT NULL,
"public_key" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "chat_messages" ADD COLUMN "sender_did" text;--> statement-breakpoint
ALTER TABLE "chat_device_bundles" ADD CONSTRAINT "chat_device_bundles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "chat_one_time_keys" ADD CONSTRAINT "chat_one_time_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "chat_one_time_keys" ADD CONSTRAINT "chat_one_time_keys_bundle_id_chat_device_bundles_id_fk" FOREIGN KEY ("bundle_id") REFERENCES "public"."chat_device_bundles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "chat_bundles_user_idx" ON "chat_device_bundles" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "chat_bundles_did_idx" ON "chat_device_bundles" USING btree ("did");--> statement-breakpoint
CREATE UNIQUE INDEX "chat_bundles_device_unique" ON "chat_device_bundles" USING btree ("user_id","device_id");--> statement-breakpoint
CREATE INDEX "chat_inbox_recipient_idx" ON "chat_inbox" USING btree ("recipient_did","recipient_device_id");--> statement-breakpoint
CREATE INDEX "chat_inbox_created_idx" ON "chat_inbox" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "chat_otk_bundle_idx" ON "chat_one_time_keys" USING btree ("bundle_id");--> statement-breakpoint
CREATE UNIQUE INDEX "chat_otk_unique" ON "chat_one_time_keys" USING btree ("bundle_id","key_id");
-3
View File
@@ -1,3 +0,0 @@
-- Migration: Add user storage columns
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "storage_provider" text;
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "storage_api_key_encrypted" text;
-3
View File
@@ -1,3 +0,0 @@
-- Migration: Add logo_data and favicon_data columns for base64 storage
ALTER TABLE "nodes" ADD COLUMN IF NOT EXISTS "logo_data" text;
ALTER TABLE "nodes" ADD COLUMN IF NOT EXISTS "favicon_data" text;
@@ -1,2 +0,0 @@
-- Migration: Add storage_public_base_url column to users table
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "storage_public_base_url" text;
@@ -0,0 +1,620 @@
CREATE TABLE `blocks` (
`id` text PRIMARY KEY,
`user_id` text NOT NULL,
`blocked_user_id` text NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_blocks_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_blocks_blocked_user_id_users_id_fk` FOREIGN KEY (`blocked_user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `bot_activity_logs` (
`id` text PRIMARY KEY,
`bot_id` text NOT NULL,
`action` text NOT NULL,
`details` text NOT NULL,
`success` integer NOT NULL,
`error_message` text,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_bot_activity_logs_bot_id_bots_id_fk` FOREIGN KEY (`bot_id`) REFERENCES `bots`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `bot_content_items` (
`id` text PRIMARY KEY,
`source_id` text NOT NULL,
`external_id` text NOT NULL,
`title` text NOT NULL,
`content` text,
`url` text NOT NULL,
`published_at` integer NOT NULL,
`fetched_at` integer DEFAULT (unixepoch()) NOT NULL,
`is_processed` integer DEFAULT false NOT NULL,
`processed_at` integer,
`post_id` text,
`interest_score` integer,
`interest_reason` text,
CONSTRAINT `fk_bot_content_items_source_id_bot_content_sources_id_fk` FOREIGN KEY (`source_id`) REFERENCES `bot_content_sources`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_bot_content_items_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE SET NULL
);
--> statement-breakpoint
CREATE TABLE `bot_content_sources` (
`id` text PRIMARY KEY,
`bot_id` text NOT NULL,
`type` text NOT NULL,
`url` text NOT NULL,
`subreddit` text,
`api_key_encrypted` text,
`source_config` text,
`keywords` text,
`is_active` integer DEFAULT true NOT NULL,
`last_fetch_at` integer,
`last_error` text,
`consecutive_errors` integer DEFAULT 0 NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_bot_content_sources_bot_id_bots_id_fk` FOREIGN KEY (`bot_id`) REFERENCES `bots`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `bot_mentions` (
`id` text PRIMARY KEY,
`bot_id` text NOT NULL,
`post_id` text NOT NULL,
`author_id` text NOT NULL,
`content` text NOT NULL,
`is_processed` integer DEFAULT false NOT NULL,
`processed_at` integer,
`response_post_id` text,
`is_remote` integer DEFAULT false NOT NULL,
`remote_actor_url` text,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_bot_mentions_bot_id_bots_id_fk` FOREIGN KEY (`bot_id`) REFERENCES `bots`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_bot_mentions_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_bot_mentions_author_id_users_id_fk` FOREIGN KEY (`author_id`) REFERENCES `users`(`id`),
CONSTRAINT `fk_bot_mentions_response_post_id_posts_id_fk` FOREIGN KEY (`response_post_id`) REFERENCES `posts`(`id`)
);
--> statement-breakpoint
CREATE TABLE `bot_rate_limits` (
`id` text PRIMARY KEY,
`bot_id` text NOT NULL,
`window_start` integer NOT NULL,
`window_type` text NOT NULL,
`post_count` integer DEFAULT 0 NOT NULL,
`reply_count` integer DEFAULT 0 NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_bot_rate_limits_bot_id_bots_id_fk` FOREIGN KEY (`bot_id`) REFERENCES `bots`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `bots` (
`id` text PRIMARY KEY,
`user_id` text NOT NULL,
`owner_id` text NOT NULL,
`name` text NOT NULL,
`personality_config` text NOT NULL,
`llm_provider` text NOT NULL,
`llm_model` text NOT NULL,
`llm_endpoint` text,
`llm_api_key_encrypted` text NOT NULL,
`schedule_config` text,
`autonomous_mode` integer DEFAULT false NOT NULL,
`is_active` integer DEFAULT true NOT NULL,
`is_suspended` integer DEFAULT false NOT NULL,
`suspension_reason` text,
`suspended_at` integer,
`last_post_at` integer,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_bots_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_bots_owner_id_users_id_fk` FOREIGN KEY (`owner_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `chat_conversations` (
`id` text PRIMARY KEY,
`type` text DEFAULT 'direct' NOT NULL,
`participant1_id` text NOT NULL,
`participant2_handle` text NOT NULL,
`last_message_at` integer,
`last_message_preview` text,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_chat_conversations_participant1_id_users_id_fk` FOREIGN KEY (`participant1_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `chat_messages` (
`id` text PRIMARY KEY,
`conversation_id` text NOT NULL,
`sender_handle` text NOT NULL,
`sender_display_name` text,
`sender_avatar_url` text,
`sender_node_domain` text,
`sender_did` text,
`content` text,
`swarm_message_id` text,
`delivered_at` integer,
`read_at` integer,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_chat_messages_conversation_id_chat_conversations_id_fk` FOREIGN KEY (`conversation_id`) REFERENCES `chat_conversations`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `chat_typing_indicators` (
`id` text PRIMARY KEY,
`conversation_id` text NOT NULL,
`user_handle` text NOT NULL,
`expires_at` integer NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_chat_typing_indicators_conversation_id_chat_conversations_id_fk` FOREIGN KEY (`conversation_id`) REFERENCES `chat_conversations`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `follows` (
`id` text PRIMARY KEY,
`follower_id` text NOT NULL,
`following_id` text NOT NULL,
`ap_id` text UNIQUE,
`pending` integer DEFAULT false,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_follows_follower_id_users_id_fk` FOREIGN KEY (`follower_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_follows_following_id_users_id_fk` FOREIGN KEY (`following_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `handle_registry` (
`handle` text PRIMARY KEY,
`did` text NOT NULL,
`node_domain` text NOT NULL,
`registered_at` integer DEFAULT (unixepoch()) NOT NULL,
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `likes` (
`id` text PRIMARY KEY,
`user_id` text NOT NULL,
`post_id` text NOT NULL,
`ap_id` text UNIQUE,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_likes_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_likes_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `media` (
`id` text PRIMARY KEY,
`user_id` text NOT NULL,
`post_id` text,
`url` text NOT NULL,
`alt_text` text,
`mime_type` text,
`width` integer,
`height` integer,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_media_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_media_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `muted_nodes` (
`id` text PRIMARY KEY,
`user_id` text NOT NULL,
`node_domain` text NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_muted_nodes_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `mutes` (
`id` text PRIMARY KEY,
`user_id` text NOT NULL,
`muted_user_id` text NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_mutes_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_mutes_muted_user_id_users_id_fk` FOREIGN KEY (`muted_user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `nodes` (
`id` text PRIMARY KEY,
`domain` text NOT NULL UNIQUE,
`name` text NOT NULL,
`description` text,
`long_description` text,
`rules` text,
`banner_url` text,
`logo_url` text,
`favicon_url` text,
`logo_data` text,
`favicon_data` text,
`accent_color` text DEFAULT '#FFFFFF',
`public_key` text,
`private_key_encrypted` text,
`is_nsfw` integer DEFAULT false NOT NULL,
`turnstile_site_key` text,
`turnstile_secret_key` text,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `notifications` (
`id` text PRIMARY KEY,
`user_id` text NOT NULL,
`actor_id` text,
`actor_handle` text NOT NULL,
`actor_display_name` text,
`actor_avatar_url` text,
`actor_node_domain` text,
`target_handle` text,
`target_display_name` text,
`target_avatar_url` text,
`target_node_domain` text,
`target_is_bot` integer,
`post_id` text,
`post_content` text,
`type` text NOT NULL,
`read_at` integer,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_notifications_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_notifications_actor_id_users_id_fk` FOREIGN KEY (`actor_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_notifications_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `posts` (
`id` text PRIMARY KEY,
`user_id` text NOT NULL,
`bot_id` text,
`content` text NOT NULL,
`reply_to_id` text,
`repost_of_id` text,
`swarm_reply_to_id` text,
`swarm_reply_to_content` text,
`swarm_reply_to_author` text,
`likes_count` integer DEFAULT 0 NOT NULL,
`reposts_count` integer DEFAULT 0 NOT NULL,
`replies_count` integer DEFAULT 0 NOT NULL,
`is_nsfw` integer DEFAULT false NOT NULL,
`is_removed` integer DEFAULT false NOT NULL,
`removed_at` integer,
`removed_by` text,
`removed_reason` text,
`ap_id` text UNIQUE,
`ap_url` text,
`link_preview_url` text,
`link_preview_title` text,
`link_preview_description` text,
`link_preview_image` text,
`link_preview_type` text,
`link_preview_video_url` text,
`link_preview_media_json` text,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_posts_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_posts_bot_id_bots_id_fk` FOREIGN KEY (`bot_id`) REFERENCES `bots`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_posts_removed_by_users_id_fk` FOREIGN KEY (`removed_by`) REFERENCES `users`(`id`)
);
--> statement-breakpoint
CREATE TABLE `remote_followers` (
`id` text PRIMARY KEY,
`user_id` text NOT NULL,
`actor_url` text NOT NULL,
`inbox_url` text NOT NULL,
`shared_inbox_url` text,
`handle` text,
`activity_id` text,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_remote_followers_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `remote_follows` (
`id` text PRIMARY KEY,
`follower_id` text NOT NULL,
`target_handle` text NOT NULL,
`target_actor_url` text NOT NULL,
`inbox_url` text NOT NULL,
`activity_id` text NOT NULL,
`display_name` text,
`bio` text,
`avatar_url` text,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_remote_follows_follower_id_users_id_fk` FOREIGN KEY (`follower_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `remote_identity_cache` (
`did` text PRIMARY KEY,
`public_key` text NOT NULL,
`fetched_at` integer NOT NULL,
`expires_at` integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE `remote_likes` (
`id` text PRIMARY KEY,
`post_id` text NOT NULL,
`actor_handle` text NOT NULL,
`actor_node_domain` text NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_remote_likes_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `remote_posts` (
`id` text PRIMARY KEY,
`ap_id` text NOT NULL,
`author_handle` text NOT NULL,
`author_actor_url` text NOT NULL,
`author_display_name` text,
`author_avatar_url` text,
`content` text NOT NULL,
`published_at` integer NOT NULL,
`link_preview_url` text,
`link_preview_title` text,
`link_preview_description` text,
`link_preview_image` text,
`link_preview_type` text,
`link_preview_video_url` text,
`link_preview_media_json` text,
`media_json` text,
`fetched_at` integer DEFAULT (unixepoch()) NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `remote_reposts` (
`id` text PRIMARY KEY,
`post_id` text NOT NULL,
`actor_handle` text NOT NULL,
`actor_node_domain` text NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_remote_reposts_post_id_posts_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `reports` (
`id` text PRIMARY KEY,
`reporter_id` text,
`target_type` text NOT NULL,
`target_id` text NOT NULL,
`reason` text NOT NULL,
`status` text DEFAULT 'open' NOT NULL,
`resolved_at` integer,
`resolved_by` text,
`resolution_note` text,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_reports_reporter_id_users_id_fk` FOREIGN KEY (`reporter_id`) REFERENCES `users`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_reports_resolved_by_users_id_fk` FOREIGN KEY (`resolved_by`) REFERENCES `users`(`id`)
);
--> statement-breakpoint
CREATE TABLE `sessions` (
`id` text PRIMARY KEY,
`user_id` text NOT NULL,
`token` text NOT NULL,
`expires_at` integer NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_sessions_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `signed_action_dedupe` (
`action_id` text PRIMARY KEY,
`did` text NOT NULL,
`nonce` text NOT NULL,
`ts` integer NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `swarm_nodes` (
`id` text PRIMARY KEY,
`domain` text NOT NULL,
`name` text,
`description` text,
`logo_url` text,
`public_key` text,
`software_version` text,
`user_count` integer,
`post_count` integer,
`is_nsfw` integer DEFAULT false NOT NULL,
`discovered_via` text,
`discovered_at` integer DEFAULT (unixepoch()) NOT NULL,
`last_seen_at` integer DEFAULT (unixepoch()) NOT NULL,
`last_sync_at` integer,
`consecutive_failures` integer DEFAULT 0 NOT NULL,
`is_active` integer DEFAULT true NOT NULL,
`trust_score` integer DEFAULT 50 NOT NULL,
`is_blocked` integer DEFAULT false NOT NULL,
`block_reason` text,
`blocked_at` integer,
`capabilities` text,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `swarm_seeds` (
`id` text PRIMARY KEY,
`domain` text NOT NULL UNIQUE,
`priority` integer DEFAULT 100 NOT NULL,
`is_enabled` integer DEFAULT true NOT NULL,
`last_contact_at` integer,
`consecutive_failures` integer DEFAULT 0 NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `swarm_sync_log` (
`id` text PRIMARY KEY,
`remote_domain` text NOT NULL,
`direction` text NOT NULL,
`nodes_received` integer DEFAULT 0 NOT NULL,
`nodes_sent` integer DEFAULT 0 NOT NULL,
`handles_received` integer DEFAULT 0 NOT NULL,
`handles_sent` integer DEFAULT 0 NOT NULL,
`success` integer NOT NULL,
`error_message` text,
`duration_ms` integer,
`created_at` integer DEFAULT (unixepoch()) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `user_swarm_likes` (
`id` text PRIMARY KEY,
`user_id` text NOT NULL,
`node_domain` text NOT NULL,
`original_post_id` text NOT NULL,
`author_handle` text NOT NULL,
`author_display_name` text,
`author_avatar_url` text,
`content` text NOT NULL,
`post_created_at` integer NOT NULL,
`likes_count` integer DEFAULT 0 NOT NULL,
`reposts_count` integer DEFAULT 0 NOT NULL,
`replies_count` integer DEFAULT 0 NOT NULL,
`link_preview_url` text,
`link_preview_title` text,
`link_preview_description` text,
`link_preview_image` text,
`link_preview_type` text,
`link_preview_video_url` text,
`link_preview_media_json` text,
`media_json` text,
`liked_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_user_swarm_likes_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `user_swarm_reposts` (
`id` text PRIMARY KEY,
`user_id` text NOT NULL,
`node_domain` text NOT NULL,
`original_post_id` text NOT NULL,
`author_handle` text NOT NULL,
`author_display_name` text,
`author_avatar_url` text,
`content` text NOT NULL,
`post_created_at` integer NOT NULL,
`likes_count` integer DEFAULT 0 NOT NULL,
`reposts_count` integer DEFAULT 0 NOT NULL,
`replies_count` integer DEFAULT 0 NOT NULL,
`link_preview_url` text,
`link_preview_title` text,
`link_preview_description` text,
`link_preview_image` text,
`link_preview_type` text,
`link_preview_video_url` text,
`link_preview_media_json` text,
`media_json` text,
`reposted_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_user_swarm_reposts_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `users` (
`id` text PRIMARY KEY,
`did` text NOT NULL,
`handle` text NOT NULL,
`email` text UNIQUE,
`password_hash` text,
`display_name` text,
`bio` text,
`avatar_url` text,
`header_url` text,
`private_key_encrypted` text,
`public_key` text NOT NULL,
`node_id` text,
`is_bot` integer DEFAULT false NOT NULL,
`bot_owner_id` text,
`is_nsfw` integer DEFAULT false NOT NULL,
`nsfw_enabled` integer DEFAULT false NOT NULL,
`age_verified_at` integer,
`is_suspended` integer DEFAULT false NOT NULL,
`suspension_reason` text,
`suspended_at` integer,
`is_silenced` integer DEFAULT false NOT NULL,
`silence_reason` text,
`silenced_at` integer,
`moved_to` text,
`moved_from` text,
`migrated_at` integer,
`storage_provider` text,
`storage_endpoint` text,
`storage_public_base_url` text,
`storage_region` text,
`storage_bucket` text,
`storage_access_key_encrypted` text,
`storage_secret_key_encrypted` text,
`followers_count` integer DEFAULT 0 NOT NULL,
`following_count` integer DEFAULT 0 NOT NULL,
`posts_count` integer DEFAULT 0 NOT NULL,
`website` text,
`dm_privacy` text DEFAULT 'everyone' NOT NULL,
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
CONSTRAINT `fk_users_node_id_nodes_id_fk` FOREIGN KEY (`node_id`) REFERENCES `nodes`(`id`),
CONSTRAINT `users_bot_owner_id_users_id_fk` FOREIGN KEY (`bot_owner_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE INDEX `blocks_user_idx` ON `blocks` (`user_id`);--> statement-breakpoint
CREATE INDEX `blocks_blocked_user_idx` ON `blocks` (`blocked_user_id`);--> statement-breakpoint
CREATE INDEX `bot_activity_logs_bot_idx` ON `bot_activity_logs` (`bot_id`);--> statement-breakpoint
CREATE INDEX `bot_activity_logs_action_idx` ON `bot_activity_logs` (`action`);--> statement-breakpoint
CREATE INDEX `bot_activity_logs_created_idx` ON `bot_activity_logs` (`created_at`);--> statement-breakpoint
CREATE INDEX `bot_content_items_source_idx` ON `bot_content_items` (`source_id`);--> statement-breakpoint
CREATE INDEX `bot_content_items_processed_idx` ON `bot_content_items` (`is_processed`);--> statement-breakpoint
CREATE INDEX `bot_content_items_external_idx` ON `bot_content_items` (`external_id`);--> statement-breakpoint
CREATE INDEX `bot_content_sources_bot_idx` ON `bot_content_sources` (`bot_id`);--> statement-breakpoint
CREATE INDEX `bot_content_sources_type_idx` ON `bot_content_sources` (`type`);--> statement-breakpoint
CREATE INDEX `bot_mentions_bot_idx` ON `bot_mentions` (`bot_id`);--> statement-breakpoint
CREATE INDEX `bot_mentions_processed_idx` ON `bot_mentions` (`is_processed`);--> statement-breakpoint
CREATE INDEX `bot_mentions_created_idx` ON `bot_mentions` (`created_at`);--> statement-breakpoint
CREATE INDEX `bot_rate_limits_bot_window_idx` ON `bot_rate_limits` (`bot_id`,`window_start`);--> statement-breakpoint
CREATE INDEX `bots_user_id_idx` ON `bots` (`user_id`);--> statement-breakpoint
CREATE INDEX `bots_owner_id_idx` ON `bots` (`owner_id`);--> statement-breakpoint
CREATE INDEX `bots_active_idx` ON `bots` (`is_active`);--> statement-breakpoint
CREATE INDEX `chat_conversations_participant1_idx` ON `chat_conversations` (`participant1_id`);--> statement-breakpoint
CREATE INDEX `chat_conversations_last_message_idx` ON `chat_conversations` (`last_message_at`);--> statement-breakpoint
CREATE UNIQUE INDEX `chat_conversations_unique` ON `chat_conversations` (`participant1_id`,`participant2_handle`);--> statement-breakpoint
CREATE INDEX `chat_messages_conversation_idx` ON `chat_messages` (`conversation_id`);--> statement-breakpoint
CREATE INDEX `chat_messages_created_idx` ON `chat_messages` (`created_at`);--> statement-breakpoint
CREATE INDEX `chat_messages_swarm_id_idx` ON `chat_messages` (`swarm_message_id`);--> statement-breakpoint
CREATE INDEX `chat_typing_conversation_idx` ON `chat_typing_indicators` (`conversation_id`);--> statement-breakpoint
CREATE INDEX `chat_typing_expires_idx` ON `chat_typing_indicators` (`expires_at`);--> statement-breakpoint
CREATE UNIQUE INDEX `chat_typing_unique` ON `chat_typing_indicators` (`conversation_id`,`user_handle`);--> statement-breakpoint
CREATE INDEX `follows_follower_idx` ON `follows` (`follower_id`);--> statement-breakpoint
CREATE INDEX `follows_following_idx` ON `follows` (`following_id`);--> statement-breakpoint
CREATE INDEX `handle_registry_updated_idx` ON `handle_registry` (`updated_at`);--> statement-breakpoint
CREATE INDEX `likes_user_post_idx` ON `likes` (`user_id`,`post_id`);--> statement-breakpoint
CREATE INDEX `media_user_idx` ON `media` (`user_id`);--> statement-breakpoint
CREATE INDEX `media_post_idx` ON `media` (`post_id`);--> statement-breakpoint
CREATE INDEX `muted_nodes_user_idx` ON `muted_nodes` (`user_id`);--> statement-breakpoint
CREATE INDEX `muted_nodes_domain_idx` ON `muted_nodes` (`node_domain`);--> statement-breakpoint
CREATE INDEX `mutes_user_idx` ON `mutes` (`user_id`);--> statement-breakpoint
CREATE INDEX `mutes_muted_user_idx` ON `mutes` (`muted_user_id`);--> statement-breakpoint
CREATE INDEX `notifications_user_idx` ON `notifications` (`user_id`);--> statement-breakpoint
CREATE INDEX `notifications_created_idx` ON `notifications` (`created_at`);--> statement-breakpoint
CREATE INDEX `posts_user_id_idx` ON `posts` (`user_id`);--> statement-breakpoint
CREATE INDEX `posts_bot_id_idx` ON `posts` (`bot_id`);--> statement-breakpoint
CREATE INDEX `posts_created_at_idx` ON `posts` (`created_at`);--> statement-breakpoint
CREATE INDEX `posts_reply_to_idx` ON `posts` (`reply_to_id`);--> statement-breakpoint
CREATE INDEX `posts_removed_idx` ON `posts` (`is_removed`);--> statement-breakpoint
CREATE INDEX `posts_nsfw_idx` ON `posts` (`is_nsfw`);--> statement-breakpoint
CREATE INDEX `remote_followers_user_idx` ON `remote_followers` (`user_id`);--> statement-breakpoint
CREATE INDEX `remote_followers_actor_idx` ON `remote_followers` (`actor_url`);--> statement-breakpoint
CREATE UNIQUE INDEX `remote_followers_user_actor_unique` ON `remote_followers` (`user_id`,`actor_url`);--> statement-breakpoint
CREATE INDEX `remote_follows_follower_idx` ON `remote_follows` (`follower_id`);--> statement-breakpoint
CREATE INDEX `remote_follows_target_idx` ON `remote_follows` (`target_handle`);--> statement-breakpoint
CREATE INDEX `remote_likes_post_idx` ON `remote_likes` (`post_id`);--> statement-breakpoint
CREATE INDEX `remote_likes_actor_idx` ON `remote_likes` (`actor_handle`,`actor_node_domain`);--> statement-breakpoint
CREATE UNIQUE INDEX `remote_likes_unique` ON `remote_likes` (`post_id`,`actor_handle`,`actor_node_domain`);--> statement-breakpoint
CREATE INDEX `remote_posts_author_idx` ON `remote_posts` (`author_handle`);--> statement-breakpoint
CREATE INDEX `remote_posts_published_idx` ON `remote_posts` (`published_at`);--> statement-breakpoint
CREATE INDEX `remote_posts_ap_id_idx` ON `remote_posts` (`ap_id`);--> statement-breakpoint
CREATE INDEX `remote_reposts_post_idx` ON `remote_reposts` (`post_id`);--> statement-breakpoint
CREATE INDEX `remote_reposts_actor_idx` ON `remote_reposts` (`actor_handle`,`actor_node_domain`);--> statement-breakpoint
CREATE UNIQUE INDEX `remote_reposts_unique` ON `remote_reposts` (`post_id`,`actor_handle`,`actor_node_domain`);--> statement-breakpoint
CREATE INDEX `reports_status_idx` ON `reports` (`status`);--> statement-breakpoint
CREATE INDEX `reports_target_idx` ON `reports` (`target_type`,`target_id`);--> statement-breakpoint
CREATE INDEX `reports_reporter_idx` ON `reports` (`reporter_id`);--> statement-breakpoint
CREATE INDEX `sessions_token_idx` ON `sessions` (`token`);--> statement-breakpoint
CREATE INDEX `sessions_user_idx` ON `sessions` (`user_id`);--> statement-breakpoint
CREATE INDEX `signed_action_dedupe_created_idx` ON `signed_action_dedupe` (`created_at`);--> statement-breakpoint
CREATE INDEX `swarm_nodes_domain_idx` ON `swarm_nodes` (`domain`);--> statement-breakpoint
CREATE INDEX `swarm_nodes_active_idx` ON `swarm_nodes` (`is_active`);--> statement-breakpoint
CREATE INDEX `swarm_nodes_last_seen_idx` ON `swarm_nodes` (`last_seen_at`);--> statement-breakpoint
CREATE INDEX `swarm_nodes_trust_idx` ON `swarm_nodes` (`trust_score`);--> statement-breakpoint
CREATE INDEX `swarm_nodes_nsfw_idx` ON `swarm_nodes` (`is_nsfw`);--> statement-breakpoint
CREATE INDEX `swarm_nodes_blocked_idx` ON `swarm_nodes` (`is_blocked`);--> statement-breakpoint
CREATE INDEX `swarm_seeds_enabled_idx` ON `swarm_seeds` (`is_enabled`);--> statement-breakpoint
CREATE INDEX `swarm_seeds_priority_idx` ON `swarm_seeds` (`priority`);--> statement-breakpoint
CREATE INDEX `swarm_sync_log_remote_idx` ON `swarm_sync_log` (`remote_domain`);--> statement-breakpoint
CREATE INDEX `swarm_sync_log_created_idx` ON `swarm_sync_log` (`created_at`);--> statement-breakpoint
CREATE INDEX `user_swarm_likes_user_idx` ON `user_swarm_likes` (`user_id`,`liked_at`);--> statement-breakpoint
CREATE INDEX `user_swarm_likes_post_idx` ON `user_swarm_likes` (`node_domain`,`original_post_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `user_swarm_likes_unique` ON `user_swarm_likes` (`user_id`,`node_domain`,`original_post_id`);--> statement-breakpoint
CREATE INDEX `user_swarm_reposts_user_idx` ON `user_swarm_reposts` (`user_id`,`reposted_at`);--> statement-breakpoint
CREATE INDEX `user_swarm_reposts_post_idx` ON `user_swarm_reposts` (`node_domain`,`original_post_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `user_swarm_reposts_unique` ON `user_swarm_reposts` (`user_id`,`node_domain`,`original_post_id`);--> statement-breakpoint
CREATE INDEX `users_handle_idx` ON `users` (`handle`);--> statement-breakpoint
CREATE INDEX `users_did_idx` ON `users` (`did`);--> statement-breakpoint
CREATE INDEX `users_suspended_idx` ON `users` (`is_suspended`);--> statement-breakpoint
CREATE INDEX `users_silenced_idx` ON `users` (`is_silenced`);--> statement-breakpoint
CREATE INDEX `users_is_bot_idx` ON `users` (`is_bot`);--> statement-breakpoint
CREATE INDEX `users_bot_owner_idx` ON `users` (`bot_owner_id`);--> statement-breakpoint
CREATE INDEX `users_nsfw_idx` ON `users` (`is_nsfw`);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-104
View File
@@ -1,104 +0,0 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1769153858323,
"tag": "0000_clumsy_donald_blake",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1769153858324,
"tag": "0004_add_source_config",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1769367465905,
"tag": "0002_add_logo_url",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1769370833410,
"tag": "0003_small_reavers",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1769377790354,
"tag": "0004_luxuriant_lockheed",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1769422771465,
"tag": "0005_yummy_millenium_guard",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1769427398729,
"tag": "0006_loud_moonstone",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1769446756226,
"tag": "0007_kind_agent_zero",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1769453302714,
"tag": "0008_illegal_carlie_cooper",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1769562092857,
"tag": "0009_sweet_chat",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1769579537338,
"tag": "0010_add_sender_did_to_chat_messages",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1769586658154,
"tag": "0011_elite_kat_farrell",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1769586699065,
"tag": "0012_quick_mockingbird",
"breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1769638500000,
"tag": "0013_add_storage_public_base_url",
"breakpoints": true
}
]
}
+5 -4
View File
@@ -1,12 +1,13 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
// Enable standalone output for Docker deployment // Turso ships a platform-native Node module and must remain a runtime dependency.
// This creates a minimal server.js that doesn't require full node_modules serverExternalPackages: ['@tursodatabase/database'],
output: 'standalone',
// Turbopack configuration // Turbopack configuration
turbopack: {}, turbopack: {
root: process.cwd(),
},
}; };
export default nextConfig; export default nextConfig;
+278 -494
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -7,9 +7,10 @@
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"dev:dev": "dotenv -e .env.dev -- next dev --turbopack -p 3050", "dev:dev": "dotenv -e .env.dev -- next dev --turbopack -p 3050",
"build": "next build", "build": "next build",
"start": "next start", "start": "npm run db:migrate && next start --hostname 127.0.0.1",
"lint": "eslint", "lint": "eslint",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:migrate": "tsx scripts/migrate.ts",
"db:generate:dev": "dotenv -e .env.dev -- drizzle-kit generate", "db:generate:dev": "dotenv -e .env.dev -- drizzle-kit generate",
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:push:dev": "dotenv -e .env.dev -- drizzle-kit push", "db:push:dev": "dotenv -e .env.dev -- drizzle-kit push",
@@ -21,19 +22,18 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.972.0", "@aws-sdk/client-s3": "^3.972.0",
"@tursodatabase/database": "0.7.0",
"@upstash/redis": "^1.34.3", "@upstash/redis": "^1.34.3",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"drizzle-kit": "^0.31.8", "drizzle-orm": "1.0.0-rc.4",
"drizzle-orm": "^0.44.1",
"jose": "^6.0.11", "jose": "^6.0.11",
"libsodium-wrappers-sumo": "^0.8.2", "libsodium-wrappers-sumo": "^0.8.2",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"multiformats": "^13.4.2", "multiformats": "^13.4.2",
"next": "16.1.4", "next": "16.1.4",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.25",
"pg": "^8.17.2",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"uuid": "^11.1.0", "uuid": "^11.1.0",
@@ -43,11 +43,11 @@
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/node": "^20.19.30", "@types/node": "^20.19.30",
"@types/pg": "^8.16.0",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"dotenv-cli": "^11.0.0", "dotenv-cli": "^11.0.0",
"drizzle-kit": "1.0.0-rc.4",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.4", "eslint-config-next": "16.1.4",
"fast-check": "^4.5.3", "fast-check": "^4.5.3",
-9
View File
@@ -1,9 +0,0 @@
import { db } from '../src/db';
async function main() {
await db.execute(`ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS sender_did text;`);
console.log('Added sender_did column to chat_messages');
process.exit(0);
}
main();
-40
View File
@@ -1,40 +0,0 @@
#!/bin/sh
# Caddy Entrypoint Script for Synapsis
# Reads the dynamic port from the shared file and starts Caddy
set -e
SHARED_PORT_FILE="/var/run/synapsis/port"
DEFAULT_PORT=3000
# Wait for the port file to exist (with timeout)
echo "⏳ Waiting for Synapsis app to announce its port..."
TIMEOUT=60
ELAPSED=0
while [ ! -f "$SHARED_PORT_FILE" ] && [ $ELAPSED -lt $TIMEOUT ]; do
sleep 1
ELAPSED=$((ELAPSED + 1))
done
if [ -f "$SHARED_PORT_FILE" ]; then
APP_PORT=$(cat "$SHARED_PORT_FILE")
echo "✅ Found Synapsis app on port: $APP_PORT"
else
echo "⚠️ Port file not found after ${TIMEOUT}s, using default port: $DEFAULT_PORT"
APP_PORT=$DEFAULT_PORT
fi
export APP_PORT
# Validate that APP_PORT is a number
if ! echo "$APP_PORT" | grep -qE '^[0-9]+$'; then
echo "❌ Invalid port number: $APP_PORT"
exit 1
fi
echo "🌐 Starting Caddy reverse proxy to app:$APP_PORT..."
echo ""
# Start Caddy with the environment variable set
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
-203
View File
@@ -1,203 +0,0 @@
#!/bin/sh
# Synapsis Docker Entrypoint Script
# Handles database migrations, port detection, and application startup
set -e
# Default port range for auto-detection
PORT_START=${PORT_START:-3000}
PORT_END=${PORT_END:-3020}
# Function to check if a port is available
check_port_available() {
local port=$1
if ! nc -z localhost "$port" 2>/dev/null; then
return 0 # Port is available
else
return 1 # Port is in use
fi
}
# Function to find first available port in range
find_available_port() {
local start=$1
local end=$2
for port in $(seq "$start" "$end"); do
if check_port_available "$port"; then
echo "$port"
return 0
fi
done
echo "ERROR: No available ports found in range $start-$end" >&2
return 1
}
# Handle PORT=auto
if [ "${PORT}" = "auto" ]; then
echo "🔍 PORT=auto detected, scanning for available port in range ${PORT_START}-${PORT_END}..."
DETECTED_PORT=$(find_available_port "$PORT_START" "$PORT_END")
if [ $? -ne 0 ]; then
echo "❌ Failed to find an available port. Exiting."
exit 1
fi
export PORT="$DETECTED_PORT"
echo "✅ Using automatically detected port: $PORT"
else
echo "📡 Using configured port: $PORT"
fi
# Ensure PORT is set
if [ -z "$PORT" ]; then
echo "⚠️ PORT not set, defaulting to 3000"
export PORT=3000
fi
# Write port to shared file for Caddy to read
SHARED_PORT_FILE="/var/run/synapsis/port"
mkdir -p "$(dirname "$SHARED_PORT_FILE")"
echo "$PORT" > "$SHARED_PORT_FILE"
echo "📝 Port $PORT written to $SHARED_PORT_FILE"
# Export HOSTNAME for Next.js
export HOSTNAME="0.0.0.0"
# ============================================
# Database Migrations
# ============================================
echo ""
echo "========================================"
echo " Synapsis - Starting Application"
echo "========================================"
echo " Time: $(date)"
echo " Working Dir: $(pwd)"
echo " Database URL: ${DATABASE_URL%%:*}://***@***"
echo " Port: $PORT"
echo "========================================"
# Ensure DATABASE_URL is set
if [ -z "$DATABASE_URL" ]; then
echo "❌ DATABASE_URL is not set. Exiting."
exit 1
fi
# Normalize domain inputs (strip scheme/path) to avoid localhost/protocol issues
sanitize_domain() {
echo "$1" | sed -E 's#^[a-zA-Z]+://##; s#/.*$##'
}
# Normalize DOMAIN if provided
if [ -n "$DOMAIN" ]; then
CLEAN_DOMAIN=$(sanitize_domain "$DOMAIN")
if [ "$CLEAN_DOMAIN" != "$DOMAIN" ]; then
export DOMAIN="$CLEAN_DOMAIN"
echo "🌐 DOMAIN normalized to $DOMAIN"
fi
fi
# Normalize NEXT_PUBLIC_NODE_DOMAIN if provided
if [ -n "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
CLEAN_NODE_DOMAIN=$(sanitize_domain "$NEXT_PUBLIC_NODE_DOMAIN")
if [ "$CLEAN_NODE_DOMAIN" != "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
export NEXT_PUBLIC_NODE_DOMAIN="$CLEAN_NODE_DOMAIN"
echo "🌐 NEXT_PUBLIC_NODE_DOMAIN normalized to $NEXT_PUBLIC_NODE_DOMAIN"
fi
fi
# Ensure NEXT_PUBLIC_NODE_DOMAIN is set (fallback to DOMAIN)
if [ -z "$NEXT_PUBLIC_NODE_DOMAIN" ] && [ -n "$DOMAIN" ]; then
export NEXT_PUBLIC_NODE_DOMAIN="$DOMAIN"
echo "🌐 NEXT_PUBLIC_NODE_DOMAIN set to $NEXT_PUBLIC_NODE_DOMAIN"
fi
# Fail fast if running in production with localhost domain
if [ "$NODE_ENV" = "production" ]; then
case "$NEXT_PUBLIC_NODE_DOMAIN" in
""|localhost|localhost:*|127.0.0.1|127.0.0.1:*)
if [ -z "$ALLOW_LOCALHOST" ]; then
echo "❌ NEXT_PUBLIC_NODE_DOMAIN is set to localhost in production."
echo " Set DOMAIN to your public domain in .env, or set ALLOW_LOCALHOST=1 to bypass."
exit 1
fi
;;
esac
fi
# Ensure NEXT_PUBLIC_APP_URL is set for background jobs
if [ -z "$NEXT_PUBLIC_APP_URL" ] && [ -n "$NEXT_PUBLIC_NODE_DOMAIN" ]; then
case "$NEXT_PUBLIC_NODE_DOMAIN" in
http://*|https://*)
NEXT_PUBLIC_APP_URL="$NEXT_PUBLIC_NODE_DOMAIN"
;;
localhost*|127.0.0.1*)
NEXT_PUBLIC_APP_URL="http://$NEXT_PUBLIC_NODE_DOMAIN"
;;
*)
NEXT_PUBLIC_APP_URL="https://$NEXT_PUBLIC_NODE_DOMAIN"
;;
esac
export NEXT_PUBLIC_APP_URL
echo "🌐 NEXT_PUBLIC_APP_URL set to $NEXT_PUBLIC_APP_URL"
fi
# Function to wait for database
wait_for_db() {
echo ""
echo "⏳ Waiting for PostgreSQL..."
# Extract host and port from DATABASE_URL
DB_HOST=$(echo "$DATABASE_URL" | sed -n 's#.*@\([^/:]*\).*#\1#p')
DB_PORT=$(echo "$DATABASE_URL" | sed -n 's#.*:\([0-9][0-9]*\)/.*#\1#p')
DB_HOST=${DB_HOST:-postgres}
DB_PORT=${DB_PORT:-5432}
max_retries=30
retry_count=0
while ! nc -z "$DB_HOST" "$DB_PORT" 2>/dev/null; do
retry_count=$((retry_count + 1))
if [ $retry_count -ge $max_retries ]; then
echo "❌ Failed to connect to database after $max_retries attempts"
exit 1
fi
echo " Attempt $retry_count/$max_retries - waiting for $DB_HOST:$DB_PORT..."
sleep 2
done
echo "✅ Database is ready!"
}
# Function to run database migrations
run_migrations() {
echo ""
echo "🔄 Running database migrations..."
echo " Current directory: $(pwd)"
echo " Drizzle directory contents:"
ls -la drizzle/ 2>/dev/null || echo " (drizzle dir not found or empty)"
# Run migrations using npm script
echo " Executing: npm run db:push"
if npm run db:push 2>&1; then
echo "✅ Migration step complete"
else
echo "❌ Migration failed"
exit 1
fi
}
# Wait for database
wait_for_db
# Run migrations
run_migrations
echo "🚀 Starting Synapsis on port $PORT..."
echo ""
# Start the Next.js application
exec node server.js
-120
View File
@@ -1,120 +0,0 @@
#!/bin/sh
set -eu
IMAGE_REPO="${IMAGE_REPO:-ghcr.io/gnosyslabs/synapsis}"
PACKAGE_API="${PACKAGE_API:-/orgs/GnosysLabs/packages/container/synapsis/versions?per_page=100}"
BUILDER="${BUILDER:-}"
PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}"
DATE_PREFIX="${DATE_PREFIX:-$(date -u +%Y.%m.%d)}"
SOURCE_REPO="${SOURCE_REPO:-https://github.com/GnosysLabs/Synapsis}"
PRUNE_BUILD_CACHE="${PRUNE_BUILD_CACHE:-1}"
APP_VERSION="${APP_VERSION:-}"
EXTRA_TAGS="${EXTRA_TAGS:-}"
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "❌ Required command not found: $1" >&2
exit 1
fi
}
require_command docker
require_command gh
require_command git
CURRENT_SHA="$(git rev-parse --short HEAD)"
CURRENT_FULL_SHA="$(git rev-parse HEAD)"
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
GITHUB_URL="${SOURCE_REPO}/commit/${CURRENT_FULL_SHA}"
if [ -z "${APP_VERSION}" ]; then
existing_tags="$(
gh api "${PACKAGE_API}" --paginate --jq '.[].metadata.container.tags[]?' 2>/dev/null || true
)"
max_build=0
for tag in ${existing_tags}; do
case "${tag}" in
"${DATE_PREFIX}".*)
build_number="${tag##${DATE_PREFIX}.}"
case "${build_number}" in
''|*[!0-9]*)
;;
*)
if [ "${build_number}" -gt "${max_build}" ]; then
max_build="${build_number}"
fi
;;
esac
;;
esac
done
next_build=$((max_build + 1))
APP_VERSION="${DATE_PREFIX}.${next_build}"
fi
BUILD_ARGS="
--platform ${PLATFORMS}
--build-arg APP_VERSION=${APP_VERSION}
--build-arg APP_COMMIT=${CURRENT_FULL_SHA}
--build-arg APP_BUILD_DATE=${BUILD_DATE}
--build-arg APP_GITHUB_URL=${GITHUB_URL}
--build-arg APP_IMAGE_REPO=${IMAGE_REPO}
--build-arg APP_SOURCE_REPO=${SOURCE_REPO}
"
TAG_ARGS="
-t ${IMAGE_REPO}:latest
-t ${IMAGE_REPO}:${APP_VERSION}
-t ${IMAGE_REPO}:${CURRENT_SHA}
"
for extra_tag in ${EXTRA_TAGS}; do
TAG_ARGS="${TAG_ARGS}
-t ${IMAGE_REPO}:${extra_tag}"
done
echo "========================================"
echo " Synapsis Docker Publish"
echo "========================================"
echo " Version: ${APP_VERSION}"
echo " Commit: ${CURRENT_SHA}"
echo " Build Date: ${BUILD_DATE}"
echo " Image: ${IMAGE_REPO}"
echo " Platforms: ${PLATFORMS}"
echo "========================================"
set -- docker buildx build
if [ -n "${BUILDER}" ]; then
set -- "$@" --builder "${BUILDER}"
fi
# shellcheck disable=SC2086
set -- "$@" $BUILD_ARGS -f docker/Dockerfile
# shellcheck disable=SC2086
set -- "$@" $TAG_ARGS --push .
"$@"
echo ""
echo "✅ Published:"
echo " ${IMAGE_REPO}:latest"
echo " ${IMAGE_REPO}:${APP_VERSION}"
echo " ${IMAGE_REPO}:${CURRENT_SHA}"
for extra_tag in ${EXTRA_TAGS}; do
echo " ${IMAGE_REPO}:${extra_tag}"
done
if [ "${PRUNE_BUILD_CACHE}" = "1" ]; then
echo ""
echo "🧹 Pruning BuildKit cache"
if [ -n "${BUILDER}" ]; then
docker buildx prune --builder "${BUILDER}" -af >/dev/null
else
docker buildx prune -af >/dev/null
fi
fi
+1 -1
View File
@@ -10,7 +10,7 @@ async function main() {
console.log('--- Inspecting Remote Key ---'); console.log('--- Inspecting Remote Key ---');
const entry = await db.query.handleRegistry.findFirst({ const entry = await db.query.handleRegistry.findFirst({
where: eq(handleRegistry.did, TARGET_DID), where: { did: TARGET_DID },
}); });
if (!entry) { if (!entry) {
+1 -1
View File
@@ -47,7 +47,7 @@ async function migrateDIDs() {
// Find all users with legacy did:synapsis: format // Find all users with legacy did:synapsis: format
const legacyUsers = await db.query.users.findMany({ const legacyUsers = await db.query.users.findMany({
where: (users, { like }) => like(users.did, 'did:synapsis:%'), where: { did: { like: 'did:synapsis:%' } },
}); });
console.log(`Found ${legacyUsers.length} users with legacy DID format\n`); console.log(`Found ${legacyUsers.length} users with legacy DID format\n`);
+17
View File
@@ -0,0 +1,17 @@
import { migrate } from 'drizzle-orm/tursodatabase/migrator';
import { db } from '../src/db';
async function main() {
const result = await migrate(db, { migrationsFolder: './drizzle' });
if (result) {
throw new Error(`Database migration failed: ${JSON.stringify(result)}`);
}
console.log('Database migrations are up to date.');
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
@@ -1,6 +1,6 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { db, handleRegistry } from '@/db'; import { db, handleRegistry } from '@/db';
import { desc, eq, gt } from 'drizzle-orm'; import { desc } from 'drizzle-orm';
import { normalizeHandle } from '@/lib/federation/handles'; import { normalizeHandle } from '@/lib/federation/handles';
export async function GET(request: Request) { export async function GET(request: Request) {
@@ -17,7 +17,7 @@ export async function GET(request: Request) {
if (handleParam) { if (handleParam) {
const cleanHandle = normalizeHandle(handleParam); const cleanHandle = normalizeHandle(handleParam);
const entry = await db.query.handleRegistry.findFirst({ const entry = await db.query.handleRegistry.findFirst({
where: eq(handleRegistry.handle, cleanHandle), where: { handle: cleanHandle },
}); });
if (!entry) { if (!entry) {
@@ -36,8 +36,8 @@ export async function GET(request: Request) {
const sinceDate = sinceParam ? new Date(sinceParam) : null; const sinceDate = sinceParam ? new Date(sinceParam) : null;
const entries = await db.query.handleRegistry.findMany({ const entries = await db.query.handleRegistry.findMany({
where: sinceDate ? gt(handleRegistry.updatedAt, sinceDate) : undefined, where: sinceDate ? { updatedAt: { gt: sinceDate } } : undefined,
orderBy: [desc(handleRegistry.updatedAt)], orderBy: () => [desc(handleRegistry.updatedAt)],
limit, limit,
}); });
+1 -192
View File
@@ -38,29 +38,6 @@ export default function AdminPage() {
const [logoUploadError, setLogoUploadError] = useState<string | null>(null); const [logoUploadError, setLogoUploadError] = useState<string | null>(null);
const [isUploadingFavicon, setIsUploadingFavicon] = useState(false); const [isUploadingFavicon, setIsUploadingFavicon] = useState(false);
const [faviconUploadError, setFaviconUploadError] = useState<string | null>(null); const [faviconUploadError, setFaviconUploadError] = useState<string | null>(null);
const [updateStatus, setUpdateStatus] = useState<{
current: { version: string; commit: string | null; buildDate: string | null };
latest: { version: string; commit: string | null; buildDate: string | null } | null;
updateAvailable: boolean;
updater: {
available: boolean;
status: string;
message?: string;
lastStartedAt?: string | null;
lastFinishedAt?: string | null;
lastExitCode?: number | null;
lastError?: string | null;
trigger?: 'manual' | 'auto' | null;
config?: {
autoUpdateEnabled: boolean;
intervalMinutes: number;
};
};
} | null>(null);
const [loadingUpdateStatus, setLoadingUpdateStatus] = useState(false);
const [triggeringUpdate, setTriggeringUpdate] = useState(false);
const [savingAutoUpdate, setSavingAutoUpdate] = useState(false);
useEffect(() => { useEffect(() => {
fetch('/api/admin/me') fetch('/api/admin/me')
.then((res) => res.json()) .then((res) => res.json())
@@ -93,42 +70,12 @@ export default function AdminPage() {
} }
}; };
const loadUpdateStatus = async () => {
setLoadingUpdateStatus(true);
try {
const res = await fetch('/api/admin/update', { cache: 'no-store' });
if (!res.ok) {
throw new Error('Failed to load update status');
}
const data = await res.json();
setUpdateStatus(data);
} catch {
setUpdateStatus(null);
} finally {
setLoadingUpdateStatus(false);
}
};
useEffect(() => { useEffect(() => {
if (isAdmin) { if (isAdmin) {
loadNodeSettings(); loadNodeSettings();
loadUpdateStatus();
} }
}, [isAdmin]); }, [isAdmin]);
useEffect(() => {
if (!isAdmin) {
return;
}
const interval = window.setInterval(() => {
loadUpdateStatus();
}, 30000);
return () => window.clearInterval(interval);
}, [isAdmin]);
const handleSaveSettings = async (override?: typeof nodeSettings) => { const handleSaveSettings = async (override?: typeof nodeSettings) => {
const payload = override ?? nodeSettings; const payload = override ?? nodeSettings;
setSavingSettings(true); setSavingSettings(true);
@@ -234,33 +181,6 @@ export default function AdminPage() {
setBannerPromptError(''); setBannerPromptError('');
}; };
const handleTriggerUpdate = async () => {
setTriggeringUpdate(true);
try {
const res = await fetch('/api/admin/update', { method: 'POST', keepalive: true });
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data.error || 'Failed to start update');
}
showToast(data.message || 'Update started. Synapsis will restart shortly.', 'success');
await loadUpdateStatus();
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to start update';
if (message.toLowerCase().includes('fetch') || message.toLowerCase().includes('network')) {
showToast('Update likely started. The node is restarting, so this page may disconnect briefly.', 'success');
window.setTimeout(() => {
window.location.reload();
}, 5000);
} else {
showToast(message, 'error');
}
} finally {
setTriggeringUpdate(false);
}
};
const handleLogoUpload = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleLogoUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
event.target.value = ''; event.target.value = '';
@@ -297,38 +217,6 @@ export default function AdminPage() {
} }
}; };
const handleToggleAutoUpdate = async () => {
if (!updateStatus?.updater.available || !updateStatus.updater.config) return;
const nextValue = !updateStatus.updater.config.autoUpdateEnabled;
setSavingAutoUpdate(true);
try {
const res = await fetch('/api/admin/update', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ autoUpdateEnabled: nextValue }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data.error || 'Failed to update auto-update setting');
}
setUpdateStatus((prev) => prev ? {
...prev,
updater: {
...prev.updater,
config: data.config,
},
} : prev);
showToast(nextValue ? 'Automatic updates enabled' : 'Automatic updates disabled', 'success');
} catch (error) {
showToast(error instanceof Error ? error.message : 'Failed to update auto-update setting', 'error');
} finally {
setSavingAutoUpdate(false);
}
};
const handleFaviconUpload = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleFaviconUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
event.target.value = ''; event.target.value = '';
@@ -703,86 +591,7 @@ export default function AdminPage() {
</button> </button>
</div> </div>
<div style={{
padding: '16px',
background: 'var(--background-secondary)',
borderRadius: '8px',
border: '1px solid var(--border)',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '16px', flexWrap: 'wrap' }}>
<div>
<h2 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '6px' }}>System Update</h2>
<p style={{ fontSize: '13px', color: 'var(--foreground-secondary)', margin: 0 }}>
Keep this node on the latest published Synapsis build.
</p>
</div>
<button
className="btn btn-primary"
onClick={handleTriggerUpdate}
disabled={
triggeringUpdate ||
loadingUpdateStatus ||
updateStatus?.updater.available === false ||
updateStatus?.updater.status === 'updating' ||
!updateStatus?.updateAvailable
}
>
{triggeringUpdate || updateStatus?.updater.status === 'updating' ? 'Updating...' : 'Update Now'}
</button>
</div>
{updateStatus?.updater.available && updateStatus?.updater.config && (
<div style={{
marginTop: '16px',
padding: '12px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--background)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '16px',
}}>
<div>
<div style={{ fontSize: '13px', fontWeight: 600, marginBottom: '4px' }}>
Automatic updates
</div>
<div style={{ fontSize: '12px', color: 'var(--foreground-secondary)' }}>
Enabled by default. This node checks for updates every {updateStatus.updater.config.intervalMinutes} minutes and installs them automatically.
</div>
</div>
<button
className={`btn btn-sm ${updateStatus.updater.config.autoUpdateEnabled ? 'btn-primary' : 'btn-ghost'}`}
onClick={handleToggleAutoUpdate}
disabled={savingAutoUpdate}
style={{ flexShrink: 0 }}
>
{savingAutoUpdate
? 'Saving...'
: updateStatus.updater.config.autoUpdateEnabled
? 'Disable'
: 'Enable'}
</button>
</div>
)}
{!updateStatus?.updater.available && (
<div style={{
marginTop: '16px',
padding: '12px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--background)',
fontSize: '12px',
color: 'var(--foreground-secondary)',
}}>
One-click updates are unavailable on this host. Use:
<div style={{ marginTop: '8px', fontFamily: 'monospace', color: 'var(--foreground)' }}>
curl -fsSL https://synapsis.social/update.sh | bash
</div>
</div>
)}
</div>
</div> </div>
)} )}
+1 -4
View File
@@ -51,10 +51,7 @@ export async function POST(request: Request) {
// First get conversation IDs where user is participant1 (local user) // First get conversation IDs where user is participant1 (local user)
// For participant2, we need to check by handle since it's stored as text (can be remote) // For participant2, we need to check by handle since it's stored as text (can be remote)
const conversations = await db.query.chatConversations.findMany({ const conversations = await db.query.chatConversations.findMany({
where: or( where: { OR: [{ participant1Id: userId }, { participant2Handle: user.handle }] },
eq(chatConversations.participant1Id, userId),
eq(chatConversations.participant2Handle, user.handle)
),
}); });
const conversationIds = conversations.map(c => c.id); const conversationIds = conversations.map(c => c.id);
+1 -1
View File
@@ -48,7 +48,7 @@ export async function POST(request: Request) {
// Check if email is already taken by another user // Check if email is already taken by another user
const existingUser = await db.query.users.findFirst({ const existingUser = await db.query.users.findFirst({
where: eq(users.email, newEmail.toLowerCase()), where: { email: newEmail.toLowerCase() },
}); });
if (existingUser && existingUser.id !== user.id) { if (existingUser && existingUser.id !== user.id) {
+6 -6
View File
@@ -156,11 +156,11 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: 'This account has already been migrated' }, { status: 400 }); return NextResponse.json({ error: 'This account has already been migrated' }, { status: 400 });
} }
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000'; const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
// Fetch user's posts // Fetch user's posts
const userPosts = await db.query.posts.findMany({ const userPosts = await db.query.posts.findMany({
where: eq(posts.userId, user.id), where: { userId: user.id },
with: { with: {
media: true, media: true,
}, },
@@ -169,19 +169,19 @@ export async function POST(req: NextRequest) {
// Fetch user's following list (local and remote) // Fetch user's following list (local and remote)
const userFollowing = await db.query.follows.findMany({ const userFollowing = await db.query.follows.findMany({
where: eq(follows.followerId, user.id), where: { followerId: user.id },
with: { with: {
following: true, following: true,
}, },
}); });
const userRemoteFollowing = await db.query.remoteFollows.findMany({ const userRemoteFollowing = await db.query.remoteFollows.findMany({
where: eq(remoteFollows.followerId, user.id), where: { followerId: user.id },
}); });
// Fetch DMs // Fetch DMs
const userConversations = await db.query.chatConversations.findMany({ const userConversations = await db.query.chatConversations.findMany({
where: eq(chatConversations.participant1Id, user.id), where: { participant1Id: user.id },
with: { with: {
messages: true messages: true
} }
@@ -189,7 +189,7 @@ export async function POST(req: NextRequest) {
// Fetch Bots // Fetch Bots
const userBots = await db.query.bots.findMany({ const userBots = await db.query.bots.findMany({
where: eq(bots.ownerId, user.id), where: { ownerId: user.id },
with: { with: {
user: true, user: true,
contentSources: true, contentSources: true,
+5 -5
View File
@@ -203,7 +203,7 @@ export async function POST(req: NextRequest) {
// Check if DID already exists on this node // Check if DID already exists on this node
const existingDid = await db.query.users.findFirst({ const existingDid = await db.query.users.findFirst({
where: eq(users.did, manifest.did), where: { did: manifest.did },
}); });
if (existingDid) { if (existingDid) {
@@ -222,7 +222,7 @@ export async function POST(req: NextRequest) {
// Check if handle is available // Check if handle is available
const existingHandle = await db.query.users.findFirst({ const existingHandle = await db.query.users.findFirst({
where: eq(users.handle, handleClean), where: { handle: handleClean },
}); });
if (existingHandle) { if (existingHandle) {
@@ -232,7 +232,7 @@ export async function POST(req: NextRequest) {
}, { status: 409 }); }, { status: 409 });
} }
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000'; const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
const oldActorUrl = `https://${manifest.sourceNode}/users/${manifest.handle}`; const oldActorUrl = `https://${manifest.sourceNode}/users/${manifest.handle}`;
const newActorUrl = `https://${nodeDomain}/users/${handleClean}`; const newActorUrl = `https://${nodeDomain}/users/${handleClean}`;
@@ -253,7 +253,7 @@ export async function POST(req: NextRequest) {
// Check if this is an NSFW node and auto-enable NSFW settings // Check if this is an NSFW node and auto-enable NSFW settings
const node = await db.query.nodes.findFirst({ const node = await db.query.nodes.findFirst({
where: eq(nodes.domain, nodeDomain), where: { domain: nodeDomain },
}); });
if (node?.isNsfw) { if (node?.isNsfw) {
@@ -319,7 +319,7 @@ export async function POST(req: NextRequest) {
} else { } else {
// Local follow - look up user and add to follows table // Local follow - look up user and add to follows table
const targetUser = await db.query.users.findFirst({ const targetUser = await db.query.users.findFirst({
where: eq(users.handle, follow.handle.toLowerCase()), where: { handle: follow.handle.toLowerCase() },
}); });
if (targetUser) { if (targetUser) {
await db.insert(follows).values({ await db.insert(follows).values({
+2 -2
View File
@@ -29,7 +29,7 @@ export async function POST(req: NextRequest) {
// Find the user on this node // Find the user on this node
const user = await db.query.users.findFirst({ const user = await db.query.users.findFirst({
where: eq(users.handle, oldHandle.toLowerCase()), where: { handle: oldHandle.toLowerCase() },
}); });
if (!user) { if (!user) {
@@ -67,7 +67,7 @@ export async function POST(req: NextRequest) {
// Get all followers to notify // Get all followers to notify
const userFollowers = await db.query.follows.findMany({ const userFollowers = await db.query.follows.findMany({
where: eq(follows.followingId, user.id), where: { followingId: user.id },
with: { with: {
follower: true, follower: true,
}, },
+2 -2
View File
@@ -9,10 +9,10 @@ export async function PATCH(req: NextRequest) {
await requireAdmin(); await requireAdmin();
const data = await req.json(); const data = await req.json();
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000'; const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
let node = await db.query.nodes.findFirst({ let node = await db.query.nodes.findFirst({
where: eq(nodes.domain, domain), where: { domain: domain },
}); });
// 2. Fallback: If not found, check if there is exactly ONE node in the system. // 2. Fallback: If not found, check if there is exactly ONE node in the system.
+2 -2
View File
@@ -95,9 +95,9 @@ export async function POST(req: NextRequest) {
const dataUrl = `data:${mimeType};base64,${base64Data}`; const dataUrl = `data:${mimeType};base64,${base64Data}`;
// Get current node // Get current node
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000'; const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
let node = await db.query.nodes.findFirst({ let node = await db.query.nodes.findFirst({
where: eq(nodes.domain, domain), where: { domain: domain },
}); });
// Fallback: If not found, check if there is exactly ONE node in the system // Fallback: If not found, check if there is exactly ONE node in the system
+2 -2
View File
@@ -16,7 +16,7 @@ export async function GET() {
await requireAdmin(); await requireAdmin();
const nodes = await db.query.swarmNodes.findMany({ const nodes = await db.query.swarmNodes.findMany({
orderBy: [desc(swarmNodes.isBlocked), desc(swarmNodes.blockedAt), desc(swarmNodes.lastSeenAt)], orderBy: () => [desc(swarmNodes.isBlocked), desc(swarmNodes.blockedAt), desc(swarmNodes.lastSeenAt)],
}); });
return NextResponse.json({ return NextResponse.json({
@@ -47,7 +47,7 @@ export async function PATCH(request: NextRequest) {
const body = await request.json(); const body = await request.json();
const data = mutateNodeSchema.parse(body); const data = mutateNodeSchema.parse(body);
const domain = normalizeNodeDomain(data.domain); const domain = normalizeNodeDomain(data.domain);
const localDomain = normalizeNodeDomain(process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000'); const localDomain = normalizeNodeDomain(process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821');
if (domain === localDomain) { if (domain === localDomain) {
return NextResponse.json({ error: 'Cannot block this node itself' }, { status: 400 }); return NextResponse.json({ error: 'Cannot block this node itself' }, { status: 400 });
+1 -1
View File
@@ -24,7 +24,7 @@ export async function PATCH(request: Request, context: RouteContext) {
const data = moderationSchema.parse(body); const data = moderationSchema.parse(body);
const post = await db.query.posts.findFirst({ const post = await db.query.posts.findFirst({
where: eq(posts.id, id), where: { id: id },
}); });
if (!post) { if (!post) {
+4 -7
View File
@@ -15,19 +15,16 @@ export async function GET(request: Request) {
const status = searchParams.get('status') || 'active'; // active | removed | all const status = searchParams.get('status') || 'active'; // active | removed | all
const limit = Math.min(parseInt(searchParams.get('limit') || '25'), 50); const limit = Math.min(parseInt(searchParams.get('limit') || '25'), 50);
const where = const where = status === 'all'
status === 'active' ? undefined
? eq(posts.isRemoved, false) : { isRemoved: status === 'removed' };
: status === 'removed'
? eq(posts.isRemoved, true)
: undefined;
const results = await db.query.posts.findMany({ const results = await db.query.posts.findMany({
where, where,
with: { with: {
author: true, author: true,
}, },
orderBy: [desc(posts.createdAt)], orderBy: () => [desc(posts.createdAt)],
limit, limit,
}); });
+1 -1
View File
@@ -24,7 +24,7 @@ export async function PATCH(request: Request, context: RouteContext) {
const data = updateSchema.parse(body); const data = updateSchema.parse(body);
const report = await db.query.reports.findFirst({ const report = await db.query.reports.findFirst({
where: eq(reports.id, id), where: { id: id },
}); });
if (!report) { if (!report) {
+4 -4
View File
@@ -16,8 +16,8 @@ export async function GET(request: Request) {
const limit = Math.min(parseInt(searchParams.get('limit') || '25'), 50); const limit = Math.min(parseInt(searchParams.get('limit') || '25'), 50);
const reportRows = await db.query.reports.findMany({ const reportRows = await db.query.reports.findMany({
where: status === 'all' ? undefined : eq(reports.status, status), where: status === 'all' ? undefined : { status: status },
orderBy: [desc(reports.createdAt)], orderBy: () => [desc(reports.createdAt)],
limit, limit,
with: { with: {
reporter: true, reporter: true,
@@ -34,13 +34,13 @@ export async function GET(request: Request) {
const postTargetsRaw = postIds.length const postTargetsRaw = postIds.length
? await db.query.posts.findMany({ ? await db.query.posts.findMany({
where: inArray(posts.id, postIds), where: { id: { in: postIds } },
with: { author: true }, with: { author: true },
}) })
: []; : [];
const userTargetsRaw = userIds.length const userTargetsRaw = userIds.length
? await db.query.users.findMany({ ? await db.query.users.findMany({
where: inArray(users.id, userIds), where: { id: { in: userIds } },
}) })
: []; : [];
-82
View File
@@ -1,82 +0,0 @@
import { NextResponse } from 'next/server';
import { requireAdmin } from '@/lib/auth/admin';
import { getCurrentBuildInfo, getLatestPublishedBuild, compareBuildVersions } from '@/lib/version';
import { getHostUpdaterStatus, triggerHostUpdate, updateHostUpdaterConfig } from '@/lib/host-updater';
function isUpdateAvailable(currentVersion: string, latestVersion: string | null | undefined) {
if (!latestVersion) {
return false;
}
return compareBuildVersions(currentVersion, latestVersion) < 0;
}
export async function GET() {
try {
await requireAdmin();
const [latest, updater] = await Promise.all([
getLatestPublishedBuild(),
getHostUpdaterStatus(),
]);
const current = getCurrentBuildInfo();
return NextResponse.json({
current,
latest,
updateAvailable: isUpdateAvailable(current.version, latest?.version),
updater,
});
} catch (error) {
if (error instanceof Error && error.message === 'Admin required') {
return NextResponse.json({ error: 'Admin required' }, { status: 403 });
}
console.error('[Admin Update] Status error:', error);
return NextResponse.json({ error: 'Failed to get update status' }, { status: 500 });
}
}
export async function POST() {
try {
await requireAdmin();
const result = await triggerHostUpdate();
return NextResponse.json(result, { status: 202 });
} catch (error) {
if (error instanceof Error && error.message === 'Admin required') {
return NextResponse.json({ error: 'Admin required' }, { status: 403 });
}
console.error('[Admin Update] Trigger error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to trigger update' },
{ status: 500 }
);
}
}
export async function PATCH(request: Request) {
try {
await requireAdmin();
const body = await request.json();
if (typeof body.autoUpdateEnabled !== 'boolean') {
return NextResponse.json({ error: 'autoUpdateEnabled must be a boolean' }, { status: 400 });
}
const result = await updateHostUpdaterConfig(body.autoUpdateEnabled);
return NextResponse.json(result);
} catch (error) {
if (error instanceof Error && error.message === 'Admin required') {
return NextResponse.json({ error: 'Admin required' }, { status: 403 });
}
console.error('[Admin Update] Config error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to update auto-update settings' },
{ status: 500 }
);
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ export async function PATCH(request: Request, context: RouteContext) {
const data = moderationSchema.parse(body); const data = moderationSchema.parse(body);
const user = await db.query.users.findFirst({ const user = await db.query.users.findFirst({
where: eq(users.id, id), where: { id: id },
}); });
if (!user) { if (!user) {
+1 -1
View File
@@ -25,7 +25,7 @@ export async function GET(req: NextRequest) {
let existingUser = null; let existingUser = null;
try { try {
existingUser = await db.query.users.findFirst({ existingUser = await db.query.users.findFirst({
where: eq(users.handle, handle), where: { handle: handle },
}); });
} catch (err: any) { } catch (err: any) {
// Handle fresh installs where the users table isn't created yet. // Handle fresh installs where the users table isn't created yet.
+2 -2
View File
@@ -81,9 +81,9 @@ export async function POST(request: Request) {
); );
// Check if this is an NSFW node and auto-enable NSFW settings // Check if this is an NSFW node and auto-enable NSFW settings
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000'; const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
const node = await db.query.nodes.findFirst({ const node = await db.query.nodes.findFirst({
where: eq(nodes.domain, nodeDomain), where: { domain: nodeDomain },
}); });
if (node?.isNsfw) { if (node?.isNsfw) {
+1 -1
View File
@@ -29,7 +29,7 @@ export async function GET(
// Verify bot exists and user owns it // Verify bot exists and user owns it
const bot = await db.query.bots.findFirst({ const bot = await db.query.bots.findFirst({
where: eq(bots.id, botId), where: { id: botId },
columns: { id: true, userId: true }, columns: { id: true, userId: true },
}); });
+1 -1
View File
@@ -29,7 +29,7 @@ export async function GET(
// Verify bot exists and user owns it // Verify bot exists and user owns it
const bot = await db.query.bots.findFirst({ const bot = await db.query.bots.findFirst({
where: eq(bots.id, botId), where: { id: botId },
columns: { id: true, userId: true }, columns: { id: true, userId: true },
}); });
@@ -38,7 +38,7 @@ export async function POST(
// Verify bot exists and user owns it // Verify bot exists and user owns it
const bot = await db.query.bots.findFirst({ const bot = await db.query.bots.findFirst({
where: eq(bots.id, botId), where: { id: botId },
columns: { columns: {
id: true, id: true,
userId: true, userId: true,
+1 -1
View File
@@ -41,7 +41,7 @@ export async function GET(
// Verify bot exists and user owns it // Verify bot exists and user owns it
const bot = await db.query.bots.findFirst({ const bot = await db.query.bots.findFirst({
where: eq(bots.id, botId), where: { id: botId },
columns: { columns: {
id: true, id: true,
userId: true, userId: true,
+1 -1
View File
@@ -31,7 +31,7 @@ export async function POST(
// Verify bot exists // Verify bot exists
const bot = await db.query.bots.findFirst({ const bot = await db.query.bots.findFirst({
where: eq(bots.id, botId), where: { id: botId },
columns: { id: true, userId: true }, columns: { id: true, userId: true },
}); });

Some files were not shown because too many files have changed in this diff Show More