Hey everyone, Alex here. If you’ve been tracking the tech news this week, you probably saw the headline: the "Stop Killing Games" European Citizens' Initiative officially failed to secure EU legislative action, despite rallying over 1.3 million signatures. The initiative was a massive push to legally force publishers to leave videogames in a "functional state" when they shut down the official servers.
While this started in the gaming community, it has sent massive shockwaves through the broader software engineering, DevOps, and open-source worlds. As developers, this isn’t just about games. This is about a fundamental architectural shift in how modern software is built, distributed, and eventually abandoned.
We live in the era of the "always-online" SaaS. We spin up microservices, tie our apps to third-party APIs, rely on proprietary cloud databases, and assume our central auth servers will live forever. But what happens when the business model changes, the startup goes under, or the product is sunsetted? The code dies, the data vanishes, and the users are left with nothing but useless client-side binaries.
Today, we’re going to talk about Software Preservation from an Engineering Perspective. How do we architect modern web applications, APIs, and services so they *can* survive the death of their primary hosting infrastructure? How do we build for graceful degradation, local-first usability, and community self-hosting? Let’s dive in.
The Architectural Antipattern: The Hard Dependency
Historically, software was a static binary. You bought a CD-ROM, ran it on your local machine, and it worked indefinitely (compatibility layers notwithstanding). Today, even simple desktop tools and mobile apps are thin clients wrapped around remote APIs.
When we design systems with tight coupling to centralized cloud infrastructure, we introduce a permanent expiration date. If the centralized API returns a 503 Service Unavailable or a 404 Not Found permanently, the client app is bricked.
As engineers, we can combat this by designing with "Sunset-Ready" architectures. This means decoupling core application logic from the transport layer, adopting local-first storage engines, and providing built-in configuration overrides that allow users to point their clients to self-hosted or community-run backends.
1. Designing for Server Redirection (The "Self-Host" Switch)
The simplest way to ensure your application can survive a server shutdown is to make the API endpoint configurable. Instead of hardcoding your production domain into your client-side code, read it from a configuration file, local storage, or an environment variable—even in production builds.
Let's look at a practical example. Imagine a desktop or mobile application written in TypeScript that fetches user data from a centralized API. Instead of a hardcoded Axios or Fetch instance, we can implement an endpoint resolver that falls back to a user-defined endpoint.
// api-client.ts
interface AppConfig {
apiBaseUrl: string;
isSelfHosted: boolean;
}
class ApiClient {
private config: AppConfig;
constructor() {
this.config = this.loadConfiguration();
}
private loadConfiguration(): AppConfig {
// 1. Check local storage / config file for user-defined overrides
const customEndpoint = localStorage.getItem('CUSTOM_API_ENDPOINT');
if (customEndpoint) {
return {
apiBaseUrl: customEndpoint,
isSelfHosted: true
};
}
// 2. Fallback to default production servers
return {
apiBaseUrl: 'https://api.sysseder.com/v1',
isSelfHosted: false
};
}
async fetchUserData(userId: string) {
try {
const response = await fetch(`${this.config.apiBaseUrl}/users/${userId}`);
if (!response.ok) throw new Error('Network error');
return await response.json();
} catch (error) {
if (this.config.isSelfHosted) {
console.warn("Self-hosted instance failed. Check your local server logs.");
} else {
console.error("Primary cloud services are down.");
}
throw error;
}
}
}
By exposing a simple "Custom Server URL" field in your application's settings UI, you instantly grant your software immortality. If your company shuts down the servers, the community can spin up an open-source clone of your API and point the existing client apps directly to it.
2. Adopting Local-First Architectures
Why do we send data to the cloud first and sync it to the device second? If you reverse that paradigm—Local-First Development—your application works completely offline, and cloud synchronization becomes an optional enhancement rather than a hard dependency.
Using technologies like SQLite (via WebAssembly in the browser), RxDB, or CRDTs (Conflict-free Replicated Data Types) allows your application to function flawlessly without a network connection. If the sync server goes offline permanently, the user loses collaboration features, but they *never* lose their data or access to the software.
A Simple Local-First Sync Flow
Instead of sending POST requests directly to a remote database, write to a local database first, queue the transaction, and sync asynchronously. Here is a conceptual architecture of how this looks:
+------------------+ Write +------------------+
| User Interface | ------------> | Local SQLite/ |
| | <------------ | IndexedDB |
+------------------+ Read/Query +------------------+
|
| Sync Queue
v
+------------------+
| Sync Manager |
+------------------+
|
| (When online)
v
+------------------+
| Remote Cloud API |
+------------------+
If the "Remote Cloud API" disappears forever, the Sync Manager can fail gracefully, but the user's UI continues to read and write to the local database seamlessly.
3. Documenting and Open-Sourcing "Seed" Backends
If you are building a proprietary SaaS, open-sourcing your entire codebase upon sunsetting might not be legally or commercially viable due to licensed third-party code or intellectual property constraints. However, you can write a lightweight, open-source "Seed" backend.
A Seed backend is a bare-minimum implementation of your application's API contracts, designed to run in a Docker container. It doesn't need your proprietary ML models or complex analytics engines; it just needs to handle authentication, basic CRUD operations, and return the JSON structures your client expects.
Here is an example of a ultra-lightweight Express.js mock server that mimics a basic user-management API. If a developer distributes this alongside a sunsetted client, the client remains fully usable.
// seed-server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 8080;
app.use(express.json());
// Mock database simulating the cloud state
const localDb = {
users: {
"1": { id: "1", name: "Offline User", preferences: { theme: "dark" } }
}
};
// Emulate the cloud authentication endpoint
app.post('/api/v1/auth/login', (req, res) => {
res.json({ token: "local-bypass-token-12345", userId: "1" });
});
// Emulate user profile retrieval
app.get('/api/v1/users/:id', (req, res) => {
const user = localDb.users[req.params.id];
if (user) {
res.json(user);
} else {
res.status(404).json({ error: "User not found" });
}
});
app.listen(PORT, () => {
console.log(`[Seed Server] Running locally on http://localhost:${PORT}`);
console.log(`[Seed Server] Point your client application to this URL to continue using it.`);
});
If you bundle this seed-server.js file into a Dockerfile and put it on GitHub, your software will survive for decades, powered by the community.
The Developer's Ethical Imperative
As software engineers, we are the architects of the digital age. The code we write represents human effort, creativity, and utility. When we allow software to be permanently destroyed simply because a hosting bill went unpaid, we contribute to a digital dark age.
Even if legislation like "Stop Killing Games" doesn't pass today, the technical community can establish a standard of responsible deprecation. When you build your next system, ask yourself:
- Can this application run if our AWS account is suspended tomorrow?
- Are our API schemas documented well enough that a third-party developer could write a replacement server?
- Have we built a clear path for users to export their data in a standard, non-proprietary format (like JSON or CSV)?
Conclusion
The campaign to save games has failed to secure legal status in the EU for now, but it has highlighted a massive engineering challenge. We have to stop treating software as a temporary service and start designing it as a durable utility. By decoupling client interfaces from fixed cloud URLs, embracing local-first architectures, and preparing lightweight open-source backends, we can ensure our hard work lives on.
What are your thoughts on software preservation? Have you ever had to sunset a service, and how did you handle user data? Let’s chat in the comments below!
Keep coding, keep building, and build things that last.