DATABASE & API

Database & API

How Helix stores metadata, manages user data, and provides API access—all while keeping file contents on the decentralized permaweb.

~2 min read

Helix uses a hybrid storage model: file contents live permanently on Arweave, while metadata lives in PostgreSQL for fast queries and user-friendly features. This gives you the best of both worlds.

What We Store

Our database stores references and metadata—never file contents:

Data Type
Stored In
Why
File content (encrypted)
Arweave
Permanent, decentralized
Transaction IDs
PostgreSQL
Fast lookup by wallet
File metadata (encrypted names)
PostgreSQL
Dashboard display
Share links
PostgreSQL
Access control
Encryption keys
Your Browser
Zero-knowledge

Why PostgreSQL?

Arweave is permanent but slow for queries. PostgreSQL lets us provide instant dashboard loads, search, and filtering. If our database disappeared, your files would still exist on Arweave—you'd just need to know the transaction IDs.

Database Schema

Our schema is designed for minimal data retention:

prisma/schema.prisma
// User's file records
model File {
  id              String   @id @default(cuid())
  walletAddress   String   @map("wallet_address")

  // Arweave reference
  transactionId   String   @unique @map("transaction_id")

  // Metadata (client-encrypted)
  encryptedName   String?  @map("encrypted_name")
  mimeType        String   @map("mime_type")
  size            Int

  // Encryption status
  isEncrypted     Boolean  @default(true) @map("is_encrypted")

  // Timestamps
  createdAt       DateTime @default(now()) @map("created_at")
  updatedAt       DateTime @updatedAt @map("updated_at")

  // Relations
  shareLinks      ShareLink[]

  @@index([walletAddress])
  @@index([createdAt])
  @@map("files")
}

// Share links for files
model ShareLink {
  id              String    @id @default(cuid())
  fileId          String    @map("file_id")

  // Access controls
  expiresAt       DateTime? @map("expires_at")
  maxDownloads    Int?      @map("max_downloads")
  downloadCount   Int       @default(0) @map("download_count")

  // Encrypted key for sharing
  encryptedKey    String?   @map("encrypted_key")

  // Timestamps
  createdAt       DateTime  @default(now()) @map("created_at")

  // Relations
  file            File      @relation(fields: [fileId], references: [id])

  @@index([fileId])
  @@map("share_links")
}

API Endpoints

Helix exposes REST API endpoints for file management:

GET /api/files

List all files for the authenticated wallet.

Response
{
  "files": [
    {
      "id": "clx1234...",
      "transactionId": "abc123...",
      "encryptedName": "encrypted_base64...",
      "mimeType": "application/pdf",
      "size": 1048576,
      "isEncrypted": true,
      "createdAt": "2025-02-01T12:00:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 20
}

POST /api/files

Create a file record after Arweave upload.

Request
{
  "transactionId": "abc123...",
  "encryptedName": "encrypted_base64...",
  "mimeType": "application/pdf",
  "size": 1048576,
  "isEncrypted": true
}

DELETE /api/files/[id]

Remove a file from your dashboard (does not delete from Arweave).

Permanence Note

Deleting a file from Helix removes it from your dashboard and our database. The actual file remains on Arweave forever—that's the nature of permanent storage. Plan accordingly.

POST /api/share

Generate a share link for a file.

Request
{
  "fileId": "clx1234...",
  "expiresAt": "2025-03-01T00:00:00Z",  // Optional
  "maxDownloads": 10,                    // Optional
  "encryptedKey": "encrypted_base64..."  // For encrypted files
}
Response
{
  "shareLink": {
    "id": "clx5678...",
    "url": "https://helix.storage/share/clx5678...",
    "expiresAt": "2025-03-01T00:00:00Z",
    "maxDownloads": 10
  }
}

Authentication

API authentication uses wallet signatures—no API keys needed:

01

1. Request Nonce

GET /api/auth/nonce?wallet=ABC123... → { nonce: "random-string-12345" }

02

2. Sign with Wallet

Wallet.signMessage("Sign in to Helix: random-string-12345") → signature

03

3. Verify Signature

POST /api/auth/verify { wallet, signature, nonce } → { token: "jwt-token..." }

04

4. Authenticated Requests

GET /api/files with Authorization: Bearer jwt-token...

authentication.ts
// Server-side signature verification
import { PublicKey } from '@solana/web3.js';
import nacl from 'tweetnacl';

export function verifySignature(
  wallet: string,
  message: string,
  signature: string
): boolean {
  const publicKey = new PublicKey(wallet);
  const messageBytes = new TextEncoder().encode(message);
  const signatureBytes = Buffer.from(signature, 'base64');

  return nacl.sign.detached.verify(
    messageBytes,
    signatureBytes,
    publicKey.toBytes()
  );
}

No Passwords

Your wallet is your login. No passwords to remember, no accounts to create, no email verification. Connect your wallet and you're authenticated.

Data Lifecycle

01

Upload

File → Encrypt → Upload to Arweave → Save metadata to PostgreSQL → arweave.net/{txId}

02

Access

Dashboard → Query PostgreSQL → Get Transaction IDs → Fetch from Arweave → Decrypt in browser

03

Delete from Dashboard

Remove from PostgreSQL (File persists on Arweave - permanent by design)

04

Share

Create ShareLink in PostgreSQL → Include encrypted key if needed → Recipient accesses via share URL

Rate Limits

To ensure fair usage, API endpoints have rate limits:

EndpointLimitWindow
GET /api/files100 requests1 minute
POST /api/files30 requests1 minute
POST /api/share20 requests1 minute
GET /api/auth/nonce10 requests1 minute

Rate limits are per-wallet. If you hit a limit, wait for the window to reset. Response headers include remaining quota:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1706803200

Last updated: February 2025