The client came in with a live website already in place. For the listener, the product looked operational: player, sections, content. From an operational standpoint, the picture was different — the site was constantly in an operational risk zone.
At the start:
- public website
novoeradio.by;
- code in a single process: React SPA + Node.js + PostgreSQL + real-time broadcast update channel;
- in production — mass restarts of the application process (around 1200+ in 15 hours) at relatively low load;
- outdated runtime and signs of instability in code/configuration — not a classic case of "the site can't handle the traffic".
Separately important: there was a second website narodnoeradio.by nearby — essentially a 1-to-1 copy under a different brand. This immediately set the requirement not to spawn two independent backends, but to build in a white-label model.
The goal was not to "tweak Node and hope," but to:
- honestly analyze the current state;
- understand whether the frontend could be preserved;
- design a target backend for real load roles;
- lock in a 1-to-1 scope and a migration path with no downtime.

Diagnostic stage: from review to specification
We started the work not with "let's immediately write a new service," but with a short engineering chain. This allowed us to align on the solution before development started and avoid scope creep.
1. Brief code review
A shallow but sufficient review confirmed the main points:
- a single process is poorly suited for mixed "content API + realtime" load;
- the runtime is outdated;
- there are signs of unreliability in the code and configuration;
- restarts are a consequence of the architecture, not peak traffic.
Conclusion: updating Node alone will not solve the problem. A new backend with separation of responsibilities is needed.
2. Migration review: can the frontend be preserved
The client's key question: would the entire website need to be rewritten.
Answer: the frontend can be reused (medium complexity), provided the external API contracts and broadcast update channel are carefully preserved.
Target stack:
- Go — realtime and hot reads;
- Symfony / FrankenPHP — CMS, admin panel, content API.
The first logical extraction from the monolith: fetching broadcast metadata and distributing updates to listeners.
Migration is gradual: first the critical realtime loop, then the content backend, then decommissioning the old stack.
3. Technical audit
A full audit confirmed the verdict: the backend is in a pre-emergency state.
At the level of finding categories:
- critical — security and access issues that cannot be left as they are;
- high — architectural monolith, unstable realtime, weak error resilience;
- medium and below — technical debt, lack of tests, weak operational documentation and implicit contracts between system parts.
Separately, we documented a plan for temporary stabilization for the period until the new backend launches — so as not to "fix production with endless patches" instead of a target replacement.
4. In-depth review and product framework of the specification
At this step, we moved from pure engineering to product structure. The specification needed to reflect not only the server, but also what the site actually delivers to the listener and the editorial team:
- public sections: music, promotions, podcasts, news, programs, hosts, etc.;
- editorial panel;
- streaming and Now Playing;
- content entities;
- brand visual system;
- white-label principle: one code, different brand-configs.
It was here that we established that the second website is not a "separate project," but a second brand on a shared platform.
5. Draft specification for the new backend
We assembled the specification based on aligned decisions:
- goal — a new backend, frontend preserved at stage 1;
- 1-to-1 principle — without expanding product scope;
- Go + Symfony/FrankenPHP architecture;
- site sections, editorial panel, streaming/players, integration contracts;
- multi-brand Novoe + Narodnoe;
- migration without downtime — gradual, with the ability to safely roll back;
- an explicit block of questions that need to be confirmed with the client before final acceptance.
The overarching thesis of the entire chain: it breaks not from load — it breaks from architecture and operational discipline.
The main migration risk is not "which language to choose," but preserving contracts between frontend and backend.

What was implemented
In the end, the site was moved to a combination with separated roles for load and ease of maintenance.
Overall schema
In simplified terms, the system consists of four layers:
- React — what the listener sees: player, sections, forms, interactions;
- PHP / Symfony — site data via API and editorial panel;
- Go (realtime) — broadcast monitoring, current track, instant distribution to all open tabs;
- PostgreSQL — news, programs, tracks, settings, broadcast history.
In production, pages, API, media, and the broadcast update channel are available from a single site address — through a unified entry point.
The working path is Symfony + Go + React. The old backend was used as a behavior reference during migration and then decommissioned from the live circuit.

Roles of technologies
React — the face of the site
- homepage and sections: news, programs, podcasts, charts, promotions, about the station, etc.;
- audio player and display of current track / cover art;
- working with API: lists, likes, votes, subscriptions;
- receiving broadcast updates in real time.
In production, the interface is built and served together with the main application. In development, the frontend can be brought up separately for convenient editing.
PHP / Symfony — content and editorial
- public site API: settings, banners, news, programs, charts, tracks, promotions, quizzes and related entities;
- editorial panel: creating and editing materials, uploading covers and audio;
- program schedules and "current program" logic;
- background service tasks for site data;
- notifying the realtime service about broadcast setting changes without a full platform restart.
PHP is the primary source of truth for site content and the editorial tool. This replaces the previous Node-based content management scheme.
Go — "here and now" broadcast
Go was chosen for tasks where constant background work and many simultaneous connections matter:
- retrieving current broadcast metadata from audio streams;
- matching the broadcast string with the track catalog;
- instant delivery of updates to all open tabs;
- accompanying broadcast actions: history, playlist, sync with external track storefronts;
- accelerating some frequent reads to offload the content backend during high traffic.
Database and media
- textual and structural content — in PostgreSQL;
- covers and audio — in a separate file storage, so that application updates do not overwrite media;
- "what's playing now" is held in the realtime loop and distributed to clients; broadcast history is saved in the database.
How Now Playing works
- Audio streams and their parameters are configured in the system. Streams without stable metadata can be skipped.
- The realtime service loads the actual list of active streams.
- It periodically reads broadcast metadata and normalizes a string like "Artist — Track".
- It looks up a match in the track catalog and substitutes the cover / related data.
- If the track changes, an update is distributed to all connected clients.
- When the site opens, the player immediately receives the current state without waiting for the next track change.
- When changes are made in the editorial panel, the realtime loop updates the configuration without restarting the entire platform.
This is precisely why realtime was extracted separately: constant broadcast support and update delivery is a different type of load than "open a news article" or "save a banner."

Environments and operations
The project supports two main circuits.
Development — for the team: backend, database, realtime, convenient frontend development and safe testing of emails/subscriptions without sending "into battle".
Production — main application, realtime service, database, persistent media storage, HTTPS and request routing to the appropriate circuit.
Environments are reproducible via Docker: the same launch logic for developers and on the server, while secrets and environment parameters are not stored in the code.
What the listener and editorial team got
Public website
- styling: backgrounds, logos, banners, slides, ad blocks;
- list of audio and video streams;
- news, promotions, programs and archive, podcasts, hosts;
- charts, new releases, voting and track ratings;
- quizzes and program subscriptions;
- player with live broadcast metadata.
Editorial panel
The editorial team works in a web interface on Symfony: creating and editing materials, uploading files, managing site entities — without the previous fragile Node-based scheme.
Multi-brand
One codebase and shared logic; different brand-configs for Novoe and Narodnoe — colors, styling, brand parameters — without duplicating backend logic.
Key project decisions
- do not rewrite the frontend at stage 1 — migrate the backend behind external contracts;
- do not "treat" the monolith by updating the runtime — replace the architecture;
- extract realtime and hot reads into Go;
- leave CMS, content and editorial on Symfony;
- use the old backend only as a behavior reference during migration;
- build in white-label for Novoe + Narodnoe;
- keep product scope 1-to-1 to keep migration controllable;
- perform cutover gradually, with the ability to safely roll back, rather than a "big bang".
Result
The client received not a cosmetic fix for an unstable Node application, but a new operational model for the radio station's website:
- content and editorial — in a manageable Symfony circuit;
- "here and now" broadcast — in a separate Go service;
- the listener interface — on the already familiar React without a full redesign;
- infrastructure — in Docker with a clear dev/prod separation and safe media storage;
- the second brand — as a platform configuration, not as a second independent site "from scratch".
