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:
Why PostgreSQL?
Database Schema
Our schema is designed for minimal data retention:
// 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.
{
"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.
{
"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
POST /api/share
Generate a share link for a file.
{
"fileId": "clx1234...",
"expiresAt": "2025-03-01T00:00:00Z", // Optional
"maxDownloads": 10, // Optional
"encryptedKey": "encrypted_base64..." // For encrypted files
}{
"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:
1. Request Nonce
GET /api/auth/nonce?wallet=ABC123... → { nonce: "random-string-12345" }
2. Sign with Wallet
Wallet.signMessage("Sign in to Helix: random-string-12345") → signature
3. Verify Signature
POST /api/auth/verify { wallet, signature, nonce } → { token: "jwt-token..." }
4. Authenticated Requests
GET /api/files with Authorization: Bearer jwt-token...
// 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
Data Lifecycle
Upload
File → Encrypt → Upload to Arweave → Save metadata to PostgreSQL → arweave.net/{txId}
Access
Dashboard → Query PostgreSQL → Get Transaction IDs → Fetch from Arweave → Decrypt in browser
Delete from Dashboard
Remove from PostgreSQL (File persists on Arweave - permanent by design)
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:
| Endpoint | Limit | Window |
|---|---|---|
| GET /api/files | 100 requests | 1 minute |
| POST /api/files | 30 requests | 1 minute |
| POST /api/share | 20 requests | 1 minute |
| GET /api/auth/nonce | 10 requests | 1 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: 1706803200Last updated: February 2025