Node.js Server Proxy
Hide your mfd_srv_* key + cache tiles. Works for any Node framework — Express, Fastify, Hono, Koa, plain http.
Express
js
import express from 'express';
import { createHmac } from 'node:crypto';
const app = express();
const KEY = process.env.MFD_SRV_KEY;
const SECRET = process.env.MFD_SRV_SECRET;
function sign(method, path) {
const ts = String(Math.floor(Date.now() / 1000));
const sig = createHmac('sha256', SECRET).update(`${method}\n${path}\n${ts}`).digest('hex');
return { 'X-MFD-Timestamp': ts, 'X-MFD-Signature': sig, Authorization: `Bearer ${KEY}` };
}
app.get('/tiles/:z/:x/:y.pbf', async (req, res) => {
const path = `/tiles/${req.params.z}/${req.params.x}/${req.params.y}.pbf`;
const r = await fetch(`https://tiles.mapsfordevs.com${path}`, { headers: sign('GET', path) });
if (!r.ok) return res.status(r.status).end();
res.set('Content-Type', 'application/x-protobuf');
res.set('Cache-Control', 'public, max-age=86400, immutable');
for await (const chunk of r.body) res.write(chunk);
res.end();
});
app.listen(8080);Fastify
js
import Fastify from 'fastify';
import { createHmac } from 'node:crypto';
const app = Fastify({ logger: true });
app.get('/tiles/:z/:x/:y.pbf', async (req, reply) => {
const path = `/tiles/${req.params.z}/${req.params.x}/${req.params.y}.pbf`;
const ts = String(Math.floor(Date.now() / 1000));
const sig = createHmac('sha256', process.env.MFD_SRV_SECRET)
.update(`GET\n${path}\n${ts}`).digest('hex');
const upstream = await fetch(`https://tiles.mapsfordevs.com${path}`, {
headers: {
Authorization: `Bearer ${process.env.MFD_SRV_KEY}`,
'X-MFD-Timestamp': ts,
'X-MFD-Signature': sig
}
});
reply.header('Content-Type', 'application/x-protobuf');
reply.header('Cache-Control', 'public, max-age=86400');
return reply.send(upstream.body);
});
app.listen({ port: 8080 });Hono (Cloudflare Workers / Bun / Node)
ts
import { Hono } from 'hono';
const app = new Hono();
app.get('/tiles/:z/:x/:y.pbf', async (c) => {
const { z, x, y } = c.req.param();
const path = `/tiles/${z}/${x}/${y}.pbf`;
const ts = String(Math.floor(Date.now() / 1000));
const enc = new TextEncoder().encode(`GET\n${path}\n${ts}`);
const key = await crypto.subtle.importKey(
'raw', new TextEncoder().encode(c.env.MFD_SRV_SECRET),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
);
const sigBytes = new Uint8Array(await crypto.subtle.sign('HMAC', key, enc));
const sig = Array.from(sigBytes).map(b => b.toString(16).padStart(2, '0')).join('');
const upstream = await fetch(`https://tiles.mapsfordevs.com${path}`, {
headers: {
Authorization: `Bearer ${c.env.MFD_SRV_KEY}`,
'X-MFD-Timestamp': ts,
'X-MFD-Signature': sig
},
cf: { cacheTtl: 86400, cacheEverything: true }
});
return new Response(upstream.body, {
status: upstream.status,
headers: {
'Content-Type': 'application/x-protobuf',
'Cache-Control': 'public, max-age=86400'
}
});
});
export default app;In-memory tile cache (Express + LRU)
js
import LRU from 'lru-cache';
const cache = new LRU({ max: 10_000, ttl: 1000 * 60 * 60 * 24 });
app.get('/tiles/:z/:x/:y.pbf', async (req, res) => {
const path = `/tiles/${req.params.z}/${req.params.x}/${req.params.y}.pbf`;
const hit = cache.get(path);
if (hit) {
res.set('Content-Type', 'application/x-protobuf');
res.set('X-Cache', 'HIT');
return res.end(hit);
}
const r = await fetch(`https://tiles.mapsfordevs.com${path}`, { headers: sign('GET', path) });
const buf = Buffer.from(await r.arrayBuffer());
if (r.ok) cache.set(path, buf);
res.set('Content-Type', 'application/x-protobuf').set('X-Cache', 'MISS').end(buf);
});Don't
- Don't put
mfd_srv_*in front-end code, environment variables exposed to the browser, or CDN config. - Don't use the same key in dev + prod. Issue separate keys, restrict prod by IP.
- Don't cache 4xx / 5xx responses.