Continuing my exploration of "pure internet", I started exploring "feral" servers. Non-standard environments that are influenced by their surrounding environment.
I settled on a Raspberry Pi Zero W, a spare board floating around from an older project that was upgraded to a Zero 2 WH. Given the sparse nature of the SOC, eschewing traditional server stacks was needed. No Nginx, Apache, or anything like that. Caddy was a brief consideration, but then I had a thought.
The page I wanted to host was a simple Hono app. Exposing the Hono dev server would be the simplest solution, and the lightweight footprint meant it could run without being resource intensive. Cloudflare Tunnels solved the next piece, letting me securely expose the port.
The Pi is currently powered by an Esprit Paradise solar panel, which barely provides enough power and needs a small battery pack to supplement. Ideally the final form will be portable.
When it is live, you can access the site here: feral.pure---internet.com
The App
The core is two background services and a Hono app. First, the location service handles getting and fuzzing our coordinates:
export class LocationService {
private cache: LocationData | null = null;
private readonly CACHE_FILE = join(__dirname, "../../../cache/location.json");
private readonly UPDATE_INTERVAL = 60 * 60 * 1000; // 1 hour
private fuzzLocation(lat: number, lon: number): { latitude: number; longitude: number } {
// Generate a random distance up to FUZZ_RADIUS_MILES
const radiusMiles = Math.random() * FUZZ_RADIUS_MILES;
const angle = Math.random() * 2 * Math.PI;
// Convert to angular distance
const angularDistance = radiusMiles / EARTH_RADIUS_MILES;
// Calculate fuzzy position using spherical geometry
const lat1 = lat * (Math.PI / 180);
const lon1 = lon * (Math.PI / 180);
const lat2 = Math.asin(
Math.sin(lat1) * Math.cos(angularDistance) +
Math.cos(lat1) * Math.sin(angularDistance) * Math.cos(angle)
);
// ... coordinate calculations ...
return {
latitude: lat2 * (180 / Math.PI),
longitude: ((lon2 * (180 / Math.PI) + 540) % 360) - 180,
};
}
}
The environment service takes those coordinates and builds our ecosystem data:
export class EnvironmentService {
private cache: EnvironmentalData | null = null;
private readonly CACHE_FILE = join(__dirname, "../../../cache/ecosystem.json");
private readonly UPDATE_INTERVAL = 5 * 60 * 1000; // 5 minutes
private async updateEnvironment(): Promise<EnvironmentalData> {
const locationData = await this.locationService.getLocation();
// Get weather data from Open-Meteo
const weatherData = await this.fetchWeather(locationData);
// Get air quality if we have an API key
const airQuality = process.env.WAQI_TOKEN
? await this.fetchAirQuality(locationData)
: this.defaultAirQuality;
// Calculate sun and moon data
const now = new Date();
const times = suncalc.getTimes(now, locationData.latitude, locationData.longitude);
const moonIllum = suncalc.getMoonIllumination(now);
return {
location: { /* location data */ },
weather: { /* weather data */ },
air: airQuality,
astronomy: {
sunrise: times.sunrise.toLocaleTimeString(),
sunset: times.sunset.toLocaleTimeString(),
moonPhase: this.getMoonPhase(moonIllum.phase),
dayLength: this.formatDayLength(times.sunset - times.sunrise)
}
};
}
}
These feed into our main Hono app:
const app = new Hono();
const locationService = new LocationService();
const environmentService = new EnvironmentService(locationService);
// Serve the main page with markdown content and ecosystem data
app.get("/", async (c) => {
const home = readFileSync(join(__dirname, "content/home.md"), "utf-8");
const etc = readFileSync(join(__dirname, "content/etc.md"), "utf-8");
const data = await environmentService.getEnvironment();
return c.html(layout(`
${marked(home)}
${renderEcosystem(data)}
${marked(etc)}
`));
});
// API endpoints for direct data access
app.get("/api/environment", async (c) => c.json(await environmentService.getEnvironment()));
app.get("/api/location", async (c) => c.json(await locationService.getLocation()));
Setup
# Clone and install
git clone https://github.com/iammatthias/feral-pure-internet.git
cd feral-pure-internet
pnpm install
# Set up environment
echo "WAQI_TOKEN=your_token" > .env
# Configure Cloudflare Tunnel
cloudflared tunnel create feral
cloudflared tunnel route dns feral feral.pure---internet.com
# Run with PM2
pm2 start "pnpm dev" --name feral
pm2 save
pm2 startup systemd -u pi --hp /home/pi
After setup, the app runs at http://localhost:3000, is accessible via Cloudflare tunnel, and auto-starts on boot.
On Living With The Sun
The internet runs on servers. Always-on, always available, almost completely divorced from the physical world.
The site can only be reached when the sun is up. When the sun sets, and the battery drains, the site goes dark. No fancy load balancing, no redundancy.
The next step is making it portable. A self-contained unit that can be placed anywhere with decent sun exposure. With some extra sensors, it could really become a "feral" server.
Previous Pure Internet Experiments
NFC: https://iammatthias.com/posts/1732585567703-pure-internet-bluesky -- Store a data URL on an NFC tag. When scanned, load in browser. Data URLs aren't supported for top level navigation, so pivoted to hosting minimal HTML on IPFS with JS to bootstrap a data URL from a url param into an iframe.
Bluesky: https://iammatthias.com/posts/1732585567703-pure-internet-bluesky -- Experiment inspired by Daniel Mangum's work. Uses Bluesky's AtProtocol, specifically the Personal Data Server (PDS) and content addressable blob storage.