Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,36 @@ Happy Server is the synchronization backbone for secure Claude Code clients. It
## Features

- 🔐 **Zero Knowledge** - The server stores encrypted data but has no ability to decrypt it
- 🎯 **Minimal Surface** - Only essential features for secure sync, nothing more
- 🎯 **Minimal Surface** - Only essential features for secure sync, nothing more
- 🕵️ **Privacy First** - No analytics, no tracking, no data mining
- 📖 **Open Source** - Transparent implementation you can audit and self-host
- 🔑 **Cryptographic Auth** - No passwords stored, only public key signatures
- ⚡ **Real-time Sync** - WebSocket-based synchronization across all your devices
- 📱 **Multi-device** - Seamless session management across phones, tablets, and computers
- 🤝 **Session Sharing** - Collaborate on conversations with granular access control
- 🔔 **Push Notifications** - Notify when Claude Code finishes tasks or needs permissions (encrypted, we can't see the content)
- 🌐 **Distributed Ready** - Built to scale horizontally when needed

## How It Works

Your Claude Code clients generate encryption keys locally and use Happy Server as a secure relay. Messages are end-to-end encrypted before leaving your device. The server's job is simple: store encrypted blobs and sync them between your devices in real-time.

### Session Sharing

Happy Server supports secure collaboration through two sharing methods:

**Direct Sharing**: Share sessions with specific users by username, with three access levels:
- **View**: Read-only access to messages
- **Edit**: Can send messages but cannot manage sharing
- **Admin**: Full access including sharing management

**Public Links**: Generate shareable URLs for broader access:
- Always read-only for security
- Optional expiration dates and usage limits
- Consent-based access logging (IP/UA only logged with explicit consent)

All sharing maintains end-to-end encryption - encrypted data keys are distributed to authorized users, and the server never sees unencrypted content.

## Hosting

**You don't need to self-host!** Our free cloud Happy Server at `happy-api.slopus.com` is just as secure as running your own. Since all data is end-to-end encrypted before it reaches our servers, we literally cannot read your messages even if we wanted to. The encryption happens on your device, and only you have the keys.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@date-fns/tz": "^1.2.0",
"@fastify/bearer-auth": "^10.1.1",
"@fastify/cors": "^10.0.1",
"@fastify/rate-limit": "^10.3.0",
"@prisma/client": "^6.11.1",
"@socket.io/redis-streams-adapter": "^0.2.2",
"@types/jsonwebtoken": "^9.0.10",
Expand Down
152 changes: 152 additions & 0 deletions prisma/migrations/20260109044634_add_session_sharing/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
-- CreateEnum
CREATE TYPE "ShareAccessLevel" AS ENUM ('view', 'edit', 'admin');

-- CreateTable
CREATE TABLE "SessionShare" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"sharedByUserId" TEXT NOT NULL,
"sharedWithUserId" TEXT NOT NULL,
"accessLevel" "ShareAccessLevel" NOT NULL DEFAULT 'view',
"encryptedDataKey" BYTEA NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "SessionShare_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "SessionShareAccessLog" (
"id" TEXT NOT NULL,
"sessionShareId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"accessedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"ipAddress" TEXT,
"userAgent" TEXT,

CONSTRAINT "SessionShareAccessLog_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "PublicSessionShare" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"createdByUserId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"accessLevel" "ShareAccessLevel" NOT NULL DEFAULT 'view',
"encryptedDataKey" BYTEA NOT NULL,
"expiresAt" TIMESTAMP(3),
"maxUses" INTEGER,
"useCount" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "PublicSessionShare_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "PublicShareAccessLog" (
"id" TEXT NOT NULL,
"publicShareId" TEXT NOT NULL,
"userId" TEXT,
"accessedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"ipAddress" TEXT,
"userAgent" TEXT,

CONSTRAINT "PublicShareAccessLog_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "PublicShareBlockedUser" (
"id" TEXT NOT NULL,
"publicShareId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"blockedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"reason" TEXT,

CONSTRAINT "PublicShareBlockedUser_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "SessionShare_sharedWithUserId_idx" ON "SessionShare"("sharedWithUserId");

-- CreateIndex
CREATE INDEX "SessionShare_sharedByUserId_idx" ON "SessionShare"("sharedByUserId");

-- CreateIndex
CREATE INDEX "SessionShare_sessionId_idx" ON "SessionShare"("sessionId");

-- CreateIndex
CREATE UNIQUE INDEX "SessionShare_sessionId_sharedWithUserId_key" ON "SessionShare"("sessionId", "sharedWithUserId");

-- CreateIndex
CREATE INDEX "SessionShareAccessLog_sessionShareId_idx" ON "SessionShareAccessLog"("sessionShareId");

-- CreateIndex
CREATE INDEX "SessionShareAccessLog_userId_idx" ON "SessionShareAccessLog"("userId");

-- CreateIndex
CREATE INDEX "SessionShareAccessLog_accessedAt_idx" ON "SessionShareAccessLog"("accessedAt");

-- CreateIndex
CREATE UNIQUE INDEX "PublicSessionShare_sessionId_key" ON "PublicSessionShare"("sessionId");

-- CreateIndex
CREATE UNIQUE INDEX "PublicSessionShare_token_key" ON "PublicSessionShare"("token");

-- CreateIndex
CREATE INDEX "PublicSessionShare_token_idx" ON "PublicSessionShare"("token");

-- CreateIndex
CREATE INDEX "PublicSessionShare_sessionId_idx" ON "PublicSessionShare"("sessionId");

-- CreateIndex
CREATE INDEX "PublicShareAccessLog_publicShareId_idx" ON "PublicShareAccessLog"("publicShareId");

-- CreateIndex
CREATE INDEX "PublicShareAccessLog_userId_idx" ON "PublicShareAccessLog"("userId");

-- CreateIndex
CREATE INDEX "PublicShareAccessLog_accessedAt_idx" ON "PublicShareAccessLog"("accessedAt");

-- CreateIndex
CREATE INDEX "PublicShareBlockedUser_publicShareId_idx" ON "PublicShareBlockedUser"("publicShareId");

-- CreateIndex
CREATE INDEX "PublicShareBlockedUser_userId_idx" ON "PublicShareBlockedUser"("userId");

-- CreateIndex
CREATE UNIQUE INDEX "PublicShareBlockedUser_publicShareId_userId_key" ON "PublicShareBlockedUser"("publicShareId", "userId");

-- AddForeignKey
ALTER TABLE "SessionShare" ADD CONSTRAINT "SessionShare_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "SessionShare" ADD CONSTRAINT "SessionShare_sharedByUserId_fkey" FOREIGN KEY ("sharedByUserId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "SessionShare" ADD CONSTRAINT "SessionShare_sharedWithUserId_fkey" FOREIGN KEY ("sharedWithUserId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "SessionShareAccessLog" ADD CONSTRAINT "SessionShareAccessLog_sessionShareId_fkey" FOREIGN KEY ("sessionShareId") REFERENCES "SessionShare"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "SessionShareAccessLog" ADD CONSTRAINT "SessionShareAccessLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "PublicSessionShare" ADD CONSTRAINT "PublicSessionShare_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "PublicSessionShare" ADD CONSTRAINT "PublicSessionShare_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "PublicShareAccessLog" ADD CONSTRAINT "PublicShareAccessLog_publicShareId_fkey" FOREIGN KEY ("publicShareId") REFERENCES "PublicSessionShare"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "PublicShareAccessLog" ADD CONSTRAINT "PublicShareAccessLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "PublicShareBlockedUser" ADD CONSTRAINT "PublicShareBlockedUser_publicShareId_fkey" FOREIGN KEY ("publicShareId") REFERENCES "PublicSessionShare"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "PublicShareBlockedUser" ADD CONSTRAINT "PublicShareBlockedUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- You are about to drop the column `accessLevel` on the `PublicSessionShare` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "PublicSessionShare" DROP COLUMN "accessLevel";
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "PublicSessionShare" ADD COLUMN "logAccess" BOOLEAN NOT NULL DEFAULT false;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Warnings:

- You are about to drop the column `logAccess` on the `PublicSessionShare` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "PublicSessionShare" DROP COLUMN "logAccess",
ADD COLUMN "isConsentRequired" BOOLEAN NOT NULL DEFAULT false;
Loading