Express server for beacon device data upload to Arkiv network with IPFS file storage support.
- Device Authentication: Ed25519 signature verification (preferred), EVM personal_sign supported for legacy clients
- Deterministic Wallet Generation: PBKDF2 derives a per-device Arkiv-paying wallet from the device's pubkey hex (or EVM address) + a server-side salt
- Arkiv Integration: Upload device entities to the Arkiv network (Braga testnet by default)
- IPFS File Storage: Upload files to IPFS via Pinata and store the hash in the Arkiv entity payload
- Rate Limited: 30 req/min per IP on
/api/device/*(env-tunable) - TypeScript: Fully typed Express server
- Node.js 18+ or Bun
- Pinata API credentials (for IPFS uploads)
- Arkiv testnet access (Braga)
- Install dependencies:
yarn install- Create a
.envfile based on.env.example:
cp .env.example .env- Configure your environment variables:
PORT=3000
SERVER_SALT=your-server-salt-here-change-in-production
ARKIV_RPC_URL=https://braga.hoodi.arkiv.network/rpc
ARKIV_WS_URL=wss://braga.hoodi.arkiv.network/rpc/ws
PINATA_API_KEY=your-pinata-api-key
PINATA_SECRET_KEY=your-pinata-secret-key
CORS_ORIGINS=http://localhost:3000,http://localhost:5173yarn devyarn build
yarn startRun the test script to verify server functionality:
yarn testThe test script will:
- Test the health endpoint
- Test signature verification endpoint
- Test entity upload without file for multiple mock devices
- Test entity upload with file (IPFS)
- Test invalid signature rejection
Make sure the server is running before executing tests. You can set a custom server URL with:
SERVER_URL=http://localhost:3000 yarn testUpload device entity data to Arkiv network with optional file upload.
Request:
- Content-Type:
multipart/form-data - Body:
entity(JSON string): Device entity objectsignature(JSON string): Signature payload withmessageandsignaturefile(optional): File to upload to IPFS
Example Entity:
{
"_id": "node_<uuid>",
"nodeId": "node_<uuid>",
"devicePub": "02ab...",
"location": { "lat": 12.34, "lon": 56.78 },
"lastSeen": "2025-11-15T22:10:00Z",
"storage": { "freeBytes": 123456, "quota": 1073741824 },
"tags": ["field-team-1", "edge-gateway"]
}Response (Success):
{
"success": true,
"data": {
"entityKey": "...",
"txHash": "...",
"ipfsHash": "..." // if file was uploaded
}
}Response (Insufficient Funds - 402):
{
"error": "Insufficient funds: The server wallet does not have enough funds to execute this transaction.",
"walletAddress": "0x413264d93a99CFD5B2159B2DA1109Bad02301AE6",
"message": "Please fund the wallet address: 0x413264d93a99CFD5B2159B2DA1109Bad02301AE6",
"faucetUrl": "https://braga.hoodi.arkiv.network/faucet/"
}Response (Other Errors - 500):
{
"error": "Arkiv upload failed: ...",
"walletAddress": "0x..." // Included for debugging
}Verify device signature without uploading.
Request:
{
"signature": {
"message": "message to verify",
"signature": "0x..."
}
}Response:
{
"success": true,
"deviceAddress": "0x..."
}Health check endpoint.
- Device signs a message with its EVM private key
- Server verifies signature and extracts device address
- Server generates deterministic wallet from device address + server salt using PBKDF2
- Server uses generated wallet to interact with Arkiv network
- Device uploads file via multipart form data
- Server uploads file to IPFS via Pinata
- Server stores IPFS hash in Arkiv entity payload
- Temporary file is cleaned up
src/
├── index.ts # Express app entry point
├── routes/
│ └── device.ts # Device API routes
├── middleware/
│ └── upload.ts # Multer file upload configuration
├── utils/
│ ├── signature.ts # Signature verification & wallet generation
│ ├── arkiv.ts # Arkiv SDK integration
│ └── ipfs.ts # Pinata IPFS upload
└── types/
└── index.ts # TypeScript type definitions
| Variable | Description | Required |
|---|---|---|
SERVER_SALT |
Salt for PBKDF2 wallet derivation. Generate once, never rotate. | Yes |
PINATA_API_KEY |
Pinata API key | Yes (for file uploads) |
PINATA_SECRET_KEY |
Pinata secret key | Yes (for file uploads) |
PORT |
Server port | No (default: 3000) |
NODE_ENV |
Standard Node convention | No (development) |
ARKIV_RPC_URL |
Arkiv RPC endpoint | No (default: Braga testnet) |
CORS_ORIGINS |
Allowed CORS origins (comma-separated) | No (default: *) |
LOG_LEVEL |
Winston log level | No (info) |
RATE_LIMIT_WINDOW_MS |
Rate-limit window | No (60000) |
RATE_LIMIT_MAX |
Max requests per window per IP | No (30) |
RATE_LIMIT_DISABLED |
Disable rate limiter (load tests only) | No |
ALLOW_BYPASS_SIGNATURE |
When true, accepts bypassSignature=true from clients. Never enable in prod. |
No (false) |
MOCK_DEVICE_PRIVATE_KEY |
Override the dev mock key used when bypass is enabled | No |
DOMAIN |
Public hostname for Caddy / Let's Encrypt (compose only) | Yes (compose) |
ACME_EMAIL |
Contact email for Let's Encrypt (compose only) | Yes (compose) |
The repo ships a docker-compose.yml + Caddyfile that runs the server behind
Caddy. Caddy auto-issues and renews a Let's Encrypt cert for your domain, so
the iOS app gets a real HTTPS endpoint with no manual cert handling.
- AMI: Amazon Linux 2023, Ubuntu 22.04, or any modern Linux.
t3.smallis enough for testing; bump tot3.mediumif BLE-spammy clients show up. - Security group inbound:
- TCP 22 from your IP (SSH)
- TCP 80 from anywhere (Let's Encrypt HTTP-01 challenge — required)
- TCP 443 from anywhere (HTTPS)
- Do not expose 3000 publicly.
- DNS: Point an A record (e.g.
beacon.example.com) at the instance's public IP. The cert won't issue until DNS resolves.
Amazon Linux 2023:
sudo dnf install -y docker
sudo systemctl enable --now docker
sudo usermod -aG docker ec2-user
# log out / back in so the group takes effectUbuntu 22.04:
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker ubuntuThe Compose plugin ships with modern Docker. Verify with docker compose version.
git clone <this-repo> beacon-server
cd beacon-server
cp .env.example .envEdit .env and set, at minimum:
DOMAIN=beacon.your-domain.com
ACME_EMAIL=ops@your-domain.com
SERVER_SALT=<openssl rand -hex 32>
PINATA_API_KEY=...
PINATA_SECRET_KEY=...
NODE_ENV=production
ALLOW_BYPASS_SIGNATURE=falsedocker compose up -d --build
docker compose logs -fFirst boot Caddy will fetch the cert; you should see certificate obtained successfully within ~30s. Once that's in, hit https://$DOMAIN/health from any browser — should return {"status":"ok",...}.
In MeshBeacon/Configs/Local.xcconfig (copy Local.xcconfig.example if missing):
BACKEND_BASE_URL = https:/$()/beacon.your-domain.com
The $() escapes the // from xcconfig comment parsing — required.
Each device that uploads triggers PBKDF2 → a unique Arkiv-paying wallet. The 402 error response includes the wallet address and faucet URL; fund it once at https://braga.hoodi.arkiv.network/faucet/ and that device can publish from then on.
- Persistence: SQLite (chat mirror) is on the named volume
beacon_data, certs oncaddy_data. Both survivedocker compose down. Snapshot your EBS volume periodically. - Updates:
git pull && docker compose up -d --build— Caddy stays up, beacon container is rebuilt and rolled. - Logs:
docker compose logs -f beacon(app),docker compose logs -f caddy(HTTPS / Let's Encrypt). - Rotating SERVER_SALT: don't. It changes every derived wallet, orphaning any funded ones. Generate once, store in a password manager.
ISC