Southside Records is a record label in a Los Santos roleplay community. Its public site shows artists, releases, and events. Staff manage that content through a separate admin experience built with React, Vite, and shadcn/ui on top of Supabase.

The original write-up described ten database tables, eight admin panels, and 43 service methods. Those figures describe the version documented then; they are not new measurements of the current site.

The part I want to explain is the connection between editing content and recording who changed it. They are related operations. In this implementation, they are not one indivisible operation.

01 / FIELD NOTESPut the content workflow in the data layer

The admin console covers artists, releases, events, photos, team, messages, logs, and a map. The public site presents the roster, releases, and events, including outbound links to Spotify, Apple Music, YouTube Music, and SoundCloud.

The data layer wraps Supabase calls in small API objects. After a create, update, or delete, it attempts to write an activity entry. Keeping that work in the service layer means the logging attempt belongs with the mutation, rather than being scattered across the interface.

EXPLORE THE CONTENT SYSTEM

Eight panels. Different jobs.

01–08

The public roster

Artists

Staff manage the artist records presented in the public roster.

CONTENT SCOPE / NOT AN ADMIN SCREENSHOT
Read every panel description
Artists
Staff manage the artist records presented in the public roster.
Releases
Release records connect the public catalog to Spotify, Apple Music, YouTube Music, and SoundCloud. These are outbound links.
Events
The site presents events; a dedicated admin panel manages this content.
Photos
Uploads live in Supabase Storage. Matching image URLs can be routed through the image Worker.
Team
A team panel is included in the documented admin scope. The source does not spell out a more detailed workflow.
Messages
The back office includes contact-enquiry management. No messaging integration beyond the documented scope is assumed.
Logs
The data layer attempts to record changes. Logging is best-effort; a failed log write does not undo a successful content change.
Map
A map panel appears in the original eight-panel inventory. Its detailed behavior is not described in the available account.
Select a panel to see its documented role. These are areas of the CMS, not eight invented database relationships.

02 / FIELD NOTESRead the order of operations

The artist-creation excerpt is short enough to show the important distinction. First, insert the artist and check for an error. Then try to write the log. Finally, return the artist.

src/services/api.ts / TypeScript
create: async (artist, userEmail?) => {
  const { data, error } = await supabase
    .from('artists').insert(artist).select().single();
  if (error) throw error;
  try {
    await adminLogsApi.create({
      action: 'create', entity_type: 'artist', entity_id: data.id,
      details: `Created artist "${data.name}"`, user_email: userEmail,
    });
  } catch (logError) { console.error("Error logging action:", logError); }
  return data as Artist;
}
Source excerpt · not executed in this design sample · scroll code horizontally on narrow screens

The try / catch surrounds the log write, not the artist insert. If the insert reports an error, the function throws. If the artist is created but logging fails, the error is written to the console and the function still returns the artist.

That behavior is easy to miss behind a phrase like “audit logging.” A saved artist does not necessarily mean there is a corresponding audit row.

LOCAL FAILURE EXPLORER

Saved content ≠ saved history.

01 / REQUEST

Create an artist

Staff submits a change.

02 / PRIMARY WRITE

Save the artist

Ready to illustrate.

03 / SEPARATE WRITE

Attempt the log

After a successful insert.

Try the failure path.

The artist is inserted first. The log is attempted in its own try / catch. Select an outcome to inspect what remains.

Read all three outcomes
Both writes succeed
The artist and activity row are saved. This illustrates one successful execution, not a guarantee for all changes.
The log write fails
The artist remains saved. The logging error is caught and written to the console. The function returns the artist without a corresponding audit row.
The content write fails
The insert error is thrown before the log attempt. The function does not reach the log write.
Simulation of the supplied code’s control flow. No artist, log, or database is changed. Best-effort logging is not a complete audit trail.

03 / FIELD NOTESBest-effort is a real limitation

The tradeoff here is concrete: a failed activity-log write does not block a successful content change. That keeps the primary change independent of the second write, but it also leaves room for gaps in the history.

I would describe the feature as best-effort activity logging, not promise that every change can be reconstructed from the log. The corrected source account makes the same distinction.

A requirement for complete history would need a stronger design, such as atomic writes or durable retries with failure monitoring. Those are possible improvements named in the source—not mechanisms this example already implements. An interface cannot turn two separately handled writes into a guarantee by displaying a reassuring badge.

04 / FIELD NOTESKeep image storage separate from delivery

Photos are uploaded to Supabase Storage. The public-facing URL can take a different path: a helper recognizes matching storage URLs and rewrites them to a Cloudflare Worker endpoint for cached delivery.

src/lib/utils.ts / TypeScript
export function getImageWorkerUrl(url) {
  if (!url) return null;
  const match = url.match(storagePattern); // matches the Supabase storage bucket
  if (match && match[1]) {
    return `https://southside-records.mjd072.workers.dev${match[1]}`;
  }
  return url;
}
Source excerpt · not executed in this design sample · scroll code horizontally on narrow screens

The conditions matter. An empty URL returns null. A matching storage URL is rewritten. A URL that does not match is returned unchanged.

The published account describes site images using this helper. The excerpt does not show the Worker’s cache configuration, a resizing pipeline, or a benchmark. It explains the URL routing, not a measured speed improvement.

IMAGE URL ROUTING

Storage and delivery are separate.

WHERE UPLOADS LIVE Supabase Storage
HELPER RETURNS Image Worker URL

A matching storage URL is rewritten to the Worker endpoint for cached delivery. This is not an upload through the Worker.

Other input cases

A nonmatching URL is returned unchanged. Empty input returns null. The source excerpt does not include the Worker’s cache configuration.

The helper’s documented branches. No image request is made, and no speed improvement is measured in this demonstration.

05 / FIELD NOTESThe work I would do next

There were no automated tests in the original account. I would start with the audit wrapper and URL helper: the insert-failure, log-failure, matching-URL, and nonmatching-URL cases are all visible in the excerpts.

The source also notes that access control relies on Supabase row-level-security policies held in the database, not in the repository. Bringing those policies into version control was an identified improvement. This article does not verify those policies or imply that the UI alone enforces permissions.

Finally, the per-entity API objects repeat a lot of structure. A small shared factory was the proposed way to reduce that repetition. The useful goal is a data layer that remains easy to inspect—not an abstraction added just to shorten a file.

06 / FIELD NOTESWhat the public page does not show

The record-label website is the visible result. Maintaining it involves content types, staff tools, storage paths, and failure handling.

That is why the log example deserves as much attention as a screenshot. It shows what the system does when one operation succeeds and the next one fails. Being precise about that behavior makes the project easier to understand—and easier to improve.

Source, dates, and what this page verifies

This is an editorial rewrite of the supplied project and technical article, describing the account published June 28, 2026. Original dates, code excerpts, and unfinished work are retained. The production repository, permissions, costs, and historical measurements have not been re-audited.

Interactive figures are local explanations. They do not call an API, send input, create content, or demonstrate production test coverage. The cumulative reference is not deployed.

Open the published source ↗