Skip to content

stephancill/eth-private-data-repo

Repository files navigation

Private Data Repo

A web app for storing key-value pairs with Ethereum wallet authentication using Sign-In with Ethereum (SIWE). Each key can be marked as public or private, with private keys requiring OAuth scopes for access.

Features

  • Ethereum Authentication: Sign in with your Ethereum wallet using SIWE (EIP-4361)
  • Key-Value Storage: Store arbitrary JSON data with user-defined keys
  • Public/Private Visibility: Mark keys as public (accessible without auth) or private
  • Per-Key Scopes: Private keys require <key>:read OAuth scope for external access
  • Smart Contract Wallet Support: EIP-1271 signature verification for Safe, Argent, etc.
  • Persistent Storage: SQLite database with Railway volume support

Tech Stack

Frontend

  • React + Vite
  • wagmi/viem for Ethereum interactions
  • TanStack Query for async state management

Backend

  • Bun runtime
  • Hono web framework
  • SQLite (bun:sqlite)
  • jose for JWT handling

Architecture

┌─────────────────────────────────────────────────────────────┐
│                        Frontend                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │   wagmi     │  │  React      │  │  TanStack Query     │  │
│  │   (wallet)  │  │  (UI)       │  │  (state)            │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                     Hono API Server                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │  Auth       │  │  Key-Value  │  │  Vite Dev Server    │  │
│  │  /api/auth  │  │  /api/keys  │  │  (dev only)         │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                        SQLite                               │
│  ┌─────────────┐  ┌─────────────┐                           │
│  │ key_values  │  │   nonces    │                           │
│  └─────────────┘  └─────────────┘                           │
└─────────────────────────────────────────────────────────────┘

Authentication Flow

sequenceDiagram
    participant User
    participant App
    participant Wallet
    participant Server

    User->>App: Connect wallet
    App->>Wallet: Request connection
    Wallet-->>App: Connected (address)
    App->>Server: GET /api/auth/nonce
    Server-->>App: { nonce }
    App->>App: Build SIWE message
    App->>Wallet: Sign message
    Wallet->>User: Approve signature?
    User->>Wallet: Approve
    Wallet-->>App: signature
    App->>Server: POST /api/auth/token { message, signature }
    Server->>Server: Verify signature (EOA/EIP-1271)
    Server-->>App: { access_token, expires_in }
    App->>Server: GET /api/keys (Bearer token)
    Server-->>App: keys[]
Loading
  1. User connects wallet (MetaMask, etc.)
  2. App switches to Ethereum mainnet
  3. App fetches a nonce from /api/auth/nonce
  4. App builds a SIWE message and requests wallet signature
  5. App sends message + signature to /api/auth/token
  6. Server verifies signature using viem.verifyMessage() (supports EOA + EIP-1271)
  7. Server issues a JWT access token (1 hour expiry)
  8. Frontend includes token in Authorization: Bearer <token> header for API requests

Discoverable OAuth Flow (EIP-4361)

For server-to-server API access, the /user/:address/:key endpoint implements discoverable OAuth for private keys:

sequenceDiagram
    participant Client
    participant Wallet
    participant Server

    Client->>Server: GET /user/:address/:key
    alt Key is public
        Server-->>Client: 200 { key, value }
    else Key is private
        Server-->>Client: 401 + WWW-Authenticate header
        Note right of Client: Parse challenge:<br/>scope, token_uri,<br/>chain_id, signing_scheme
        Client->>Server: GET /api/auth/nonce
        Server-->>Client: { nonce }
        Client->>Client: Build SIWE message with scopes
        Client->>Wallet: Sign message
        Wallet-->>Client: signature
        Client->>Server: POST /api/auth/token<br/>{ grant_type: "eth_signature",<br/>message, signature, scope }
        Server->>Server: Verify signature & scopes
        Server-->>Client: { access_token, scope }
        Client->>Server: GET /user/:address/:key<br/>(Bearer token)
        Server-->>Client: { key, value }
    end
Loading
  1. Client requests the endpoint without authentication
  2. If the key is public, the value is returned immediately
  3. If the key is private, server responds with 401 Unauthorized and WWW-Authenticate header:
    WWW-Authenticate: Bearer, realm="kv-settings", scope="settings:read",
      token_uri="https://api.example.com/api/auth/token", chain_id="1", signing_scheme="eip4361"
    
  4. Client constructs a SIWE message with scopes in resources field:
    Resources:
    - urn:oauth:scope:settings:read
    
  5. Client signs the message and exchanges for a token:
    POST /api/auth/token
    {
      "grant_type": "eth_signature",
      "message": "<SIWE message>",
      "signature": "0x...",
      "scope": "settings:read"
    }
  6. Server returns an OAuth-compatible token response:
    {
      "access_token": "<jwt>",
      "token_type": "Bearer",
      "expires_in": 3600,
      "scope": "settings:read"
    }
  7. Client retries with Authorization: Bearer <token>

API Endpoints

Public

  • GET /api/auth/nonce - Get a fresh nonce for SIWE
  • POST /api/auth/token - Exchange signed SIWE message for JWT (supports eth_signature grant type)

Key-Value Access (Discoverable OAuth)

  • GET /user/:address/:key - Get a value by owner address and key
    • Public keys: Returns value without authentication
    • Private keys: Returns 401 with WWW-Authenticate challenge requiring <key>:read scope

Protected (requires Bearer token)

  • GET /api/keys - List all keys for authenticated user
  • PUT /api/keys/:key - Create or update a key-value pair
  • DELETE /api/keys/:key - Delete a key

Request/Response Examples

Create/Update a key

PUT /api/keys/profile
Authorization: Bearer <token>
Content-Type: application/json

{
  "value": {"name": "Alice", "bio": "Developer"},
  "isPublic": true
}

Response:

{
  "key": "profile",
  "value": {"name": "Alice", "bio": "Developer"},
  "isPublic": true,
  "createdAt": "2024-01-01T00:00:00Z",
  "updatedAt": "2024-01-01T00:00:00Z"
}

Get a public value (no auth required)

GET /user/0xabc.../profile

Response:

{
  "key": "profile",
  "value": {"name": "Alice", "bio": "Developer"},
  "owner": "0xabc...",
  "isPublic": true
}

Get a private value (auth required)

GET /user/0xabc.../settings
Authorization: Bearer <token with settings:read scope>

Response:

{
  "key": "settings",
  "value": {"theme": "dark", "notifications": true},
  "owner": "0xabc...",
  "isPublic": false
}

OAuth Scopes

  • <key>:read - Read a specific private key (e.g., profile:read, settings:read)
  • kv:write - Create/update/delete keys (owner operations)

Development

# Install dependencies
bun install

# Create .env file
echo "JWT_SECRET=$(openssl rand -base64 32)" > .env

# Start dev server
bun run dev

Open http://localhost:3000

Production

# Build frontend
bun run build

# Start production server
NODE_ENV=production JWT_SECRET=your-secret DATA_DIR=/app/data bun server/index.ts

Railway Deployment

  1. Connect your GitHub repo to Railway
  2. Add environment variables:
    • JWT_SECRET - Generate with openssl rand -base64 32
  3. Add a persistent volume mounted at /app/data
  4. Deploy

The app uses nixpacks.toml for build configuration and railway.json for deployment settings.

Related

About

A demo web app for storing personal messages with Ethereum wallet authentication using Sign-In with Ethereum (SIWE).

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors