2026-01-14 00:44:32
Spot instances offer compelling cost savings—often 60-70% compared to on-demand pricing. For organizations running containerized workloads, this translates to substantial infrastructure budget reductions. The business case is clear: migrate to spot instances wherever possible.
However, adopting spot instances introduces a challenging operational problem.
Spot instances terminate frequently—sometimes multiple times per day across a cluster. Each termination triggers cascading effects:
Monitoring alerts fire continuously:
Customer-facing impact:
The irony: services recover automatically through ECS's built-in resilience mechanisms, but not before generating alerts, incident tickets, and potential customer complaints.
The standard recommendation for spot resilience is straightforward: spread tasks across multiple instances using ECS placement strategies.
{
"placementStrategy": [
{"type": "spread", "field": "instanceId"}
]
}
This configuration ensures that losing one instance affects only a small percentage of total capacity. The blast radius becomes manageable.
The problem: This approach works well at scale but becomes prohibitively expensive for small-to-medium services and clusters.
Large service (100+ tasks):
Small-to-medium service (5-20 tasks):
Note: In practice, small services typically run in small clusters (one or a few services per cluster), so "small service" and "small cluster" often refer to the same deployment pattern.
The cost impact:
Organizations running small-to-medium clusters (the majority of microservices deployments) face a dilemma:
None of these options are satisfactory for small-to-medium workloads. Let's analyze this technical challenge in detail and explore how different orchestration platforms handle this scale-dependent problem.
This operational challenge can be visualized as an optimization problem with three competing objectives:
Spot Resilience
(minimize alarm fatigue
& customer impact)
/\
/ \
/ \
/ \
/ \
/ \
/____________\
Cost Auto-Scaling
Efficiency (5-20 tasks)
Challenge: Optimize for all three simultaneously
Important context: This problem is scale-dependent. Large services (50+ tasks) naturally solve this triangle—enough tasks to both spread across instances AND utilize resources efficiently. The dilemma is specific to small-to-medium clusters where individual services have 5-20 tasks, representing the majority of modern microservice deployments.
In practice, organizations discover that container orchestration platforms force trade-offs between these objectives for smaller services. Achieving all three requires either platform-specific workarounds or architectural capabilities that some platforms simply don't provide.
The most straightforward approach to eliminating alarm fatigue is maximizing task distribution:
{
"serviceName": "api-service",
"desiredCount": 10,
"capacityProviderStrategy": [{
"capacityProvider": "spot-asg-provider",
"weight": 1
}],
"placementStrategy": [
{
"type": "spread",
"field": "instanceId"
}
]
}
Behavior:
Operational impact:
Cost impact:
The paradox: This configuration solves the operational problem (no alarms, no incidents) but negates the entire financial justification for using spot instances in the first place.
To reclaim cost efficiency, the next approach focuses on resource utilization:
{
"placementStrategy": [
{
"type": "spread",
"field": "attribute:ecs.availability-zone"
},
{
"type": "binpack",
"field": "memory"
}
]
}
Behavior:
Task distribution:
Instance 1 (spot): 4 tasks
Instance 2 (spot): 3 tasks
Instance 3 (spot): 3 tasks
Cost impact:
Operational impact:
The incident pattern: When Instance 1 terminates (daily occurrence), 4 tasks disappear simultaneously. Remaining 6 tasks handle 100% of traffic, causing:
By the time engineers acknowledge the page, ECS has already recovered. But the alarm fatigue accumulates—multiple times per day, every day.
A common misconception is that targetCapacity controls task distribution:
{
"capacityProvider": "my-asg-provider",
"managedScaling": {
"targetCapacity": 60
}
}
Reality: targetCapacity determines the cluster utilization threshold for triggering scale-out, not how tasks are distributed across instances.
Behavior:
With a binpack strategy, tasks still concentrate on fewer instances. Lower targetCapacity provisions more instances but doesn't change the distribution pattern—the additional instances remain underutilized.
Use instance types with limited capacity to physically constrain task density:
{
"placementStrategy": [
{"type": "spread", "field": "instanceId"}
]
}
// ASG Configuration
// Instance type: t4g.small (2GB RAM)
// Task memory requirement: 1GB
// Physical limit: 2 tasks per instance maximum
Outcome:
Trade-off: This approach uses physical constraints as a proxy for scheduling policy, which feels architecturally inelegant.
Note: For small ECS clusters, this workaround effectively balances cost efficiency and spot protection. However, this raises a broader architectural question: should clusters use many small instances or fewer large instances? That debate involves considerations around bin-packing efficiency, operational overhead, blast radius philosophy, and AWS service limits—topics beyond the scope of this discussion. For the specific problem of spot resilience in small services, small instance types provide a pragmatic solution regardless of overall cluster architecture.
{
"capacityProviderStrategy": [
{
"capacityProvider": "on-demand-provider",
"base": 3,
"weight": 0
},
{
"capacityProvider": "spot-provider",
"base": 0,
"weight": 1
}
]
}
Outcome:
Cost:
Trade-off: Higher baseline cost for improved reliability.
Other container orchestration platforms handle this problem differently. Kubernetes, for example, provides topologySpreadConstraints that directly specify the maximum number of pods per node:
spec:
topologySpreadConstraints:
- maxSkew: 2 # Max 2 pods per node
topologyKey: kubernetes.io/hostname
This simple configuration achieves all three objectives for small-to-medium clusters:
The maxSkew parameter provides granular control (1, 2, 5, etc.) over the distribution density, enabling precise optimization along the resilience-efficiency spectrum—something ECS placement strategies cannot express directly.
The core issue isn't ECS inadequacy—it's an architectural constraint for small-to-medium clusters:
ECS lacks granular per-instance task limits.
Available strategies:
spread by instanceId = Exactly 1 task per instance (maximum spread, works well for large services)binpack = As many tasks as resources allow (maximum density)spread by AZ + binpack = Zone distribution, then density (no per-instance control)For small-to-medium clusters (5-20 tasks per service), these binary options force choosing between over-provisioning (spread) or excessive blast radius (binpack). There's no middle ground to specify "aim for 2-3 tasks per instance."
Despite these limitations, ECS is often the pragmatic choice when:
Critical insight: The "impossible triangle" primarily affects small-to-medium clusters (5-20 tasks per service). At larger scales (50+ tasks per service), spread strategies achieve both good distribution and efficient resource usage simultaneously. ECS's simpler model reduces operational complexity for straightforward use cases and scales excellently for high-volume services.
Scale-Dependent Problem: The "impossible triangle" primarily affects small-to-medium clusters (5-20 tasks per service). Large services (50+ tasks) naturally achieve both good distribution and efficient resource usage.
Root Cause: ECS lacks granular per-instance task limits—only extreme options exist (1 task/instance spread OR full binpack), with no middle ground.
Practical Workarounds: Small instance types (t4g.small) provide the most effective solution, physically limiting task density while maintaining cost efficiency ($25/month vs $250/month).
Platform Limitations: Other orchestration platforms provide granular controls that directly address this problem, highlighting an architectural constraint rather than a configuration issue.
The spot instance adoption dilemma reveals a fundamental constraint in ECS's task placement architecture: the absence of granular per-instance task limits.
The scale-dependent reality: For large-scale services (50+ tasks), ECS placement strategies work excellently—tasks naturally distribute across instances while maintaining efficient resource utilization. The "impossible triangle" problem emerges specifically for small-to-medium clusters (5-20 tasks per service) that dominate modern microservice architectures.
For these smaller clusters:
The broader lesson: Container orchestration platforms make architectural trade-offs that favor certain workload profiles. ECS's binary placement options (spread vs binpack) scale well at the extremes—either very large services or services where cost takes priority over operational stability.
Understanding these platform constraints enables realistic expectations and informed architectural decisions. When evaluating ECS for spot instance deployments, the critical question becomes: Does your cluster size align with where ECS placement strategies excel?
For small-to-medium clusters, the operational pain of alarm fatigue may ultimately outweigh the promised cost savings—making the spot instance business case less compelling than it initially appears.
Running ECS on spot instances? Struggling with alarm fatigue or over-provisioning? Share your experiences and workarounds in the comments.
Further Reading:
Connect with me on LinkedIn: https://www.linkedin.com/in/rex-zhen-b8b06632/
I share insights on cloud architecture, container orchestration, and SRE practices. Let's connect and learn together!
2026-01-14 00:42:01
Last month, Cursor launched for the fifth time on Product Hunt in 2025.
The 2024 Product of the Year still hits the charts. They have launched web and mobile agents, a visual editor, and 2.0, consistently ranking in the Top 5 Products of the Day.
I had a look at their recent launches. Here's what I found.
Straightforward tagline. The 60-character tagline might be the most important part of a launch. It's the first thing you see on the front page. What makes Cursor stand out? They highlight the features, not the benefits.
Minimalist visual assets. The image gallery is the first impression of your product. It sets expectations. Cursor's recent launches highlighted 2 to 4 images. No stock images, no marketing fluff, just product screenshots. They show the product.
Found a Hunter. Ben Lang was the hunter of their latest launches - an established user with 50K+ followers, among the 2022 Community Members of the Year (runner-up). It might help increase their reach.
Feedback first. Like the tagline and visual assets, the first comment is the simplest. No looooooong background stories, they're just genuinely curious about what the community thinks of the release.
Riding the tailwinds. Last but not least, the launch timing. They first announced the product updates on X, gained momentum, and then launched on Product Hunt the next day, riding the tailwinds.
For context, Cursor has 20K+ followers on Product Hunt. It certainly helps get more exposure. However, IMHO it's only part of the formula. Straightforward tagline, minimalist image gallery, simple comments... It all resonates with developers, their target audience. To quote Lee Robinson on what great developer marketing looks like:
Great marketing values your time; every word matters. Show them how to build interesting things; don’t fill a post with 1,000 words of garbage.
-- Lee Robinson (Vercel, Cursor)
Developers are resistant to anything that looks, sounds, or smells like marketing. Cursor keeps it simple, and it works.
Over to you! What are your key learnings from your previous product launches? What worked, what didn't work from your perspective?
2026-01-14 00:31:42
The last couple of months I've been fiddling around with Godot and did all sorts of random experiments, from educating myself and getting more comfortable with the engine, to building small prototypes, where I would challenge myself to build one specific thing first.
This journey taught me a lot about the engine and how I can utilize it in the future to build my first fully-fledged game.
I had a few ideas in mind what I wanted to create. Started with a heavy logical game with a lot of math and variables, as I wanted my first game to be a simulation game. I must admit, this was not the smartest idea for my first game but one, I'll definitely look into more in the future - not now.
In the end it came down to a game, where the player owns a fish tank, which they have to manage. It's more of a cozy idling management game, where the player feed fish, get more fish, get some upgrades to make life easier (autofeeder, cleaner, etc.), and maybe some challenges to keep the player actually engaged. The game plan is simple and a good starting point for my first game.
From my previous experiments I knew how to move a character around on a 2D scene. So I started by searching for a placeholder fish sprite, placed it in my scene and added a simple movement script to it, which moved the fish around at random directions every few seconds. I used a timer that counts down using the delta value passed to Godot's _physics_process function.
The fish extends the CharacterBody2D node, which gives us built-in physics and collision handling. I used a simple state machine (enum State) to manage different behaviors, though for now we're only implementing the WANDER state.
class_name Fish
extends CharacterBody2D
enum State {WANDER, HUNGRY, EATING, FLEEING}
@onready var pathing: Line2D = $Pathing
@export var speed: float = 100.0
@export var prediction_length: float = 200.0
@export var prediction_steps: int = 20
var current_state: State = State.WANDER
var wander_direction: Vector2 = Vector2.ZERO
var wander_timer: float = 0.0
func _ready() -> void:
_choose_new_wander_direction()
if pathing != null:
pathing.visible = OS.is_debug_build()
func _physics_process(delta: float) -> void:
match current_state:
State.WANDER:
_handle_wander(delta)
_update_visuals()
if OS.is_debug_build():
_update_path_prediction()
move_and_slide()
func _handle_wander(delta: float) -> void:
velocity = wander_direction * speed
wander_timer -= delta
if wander_timer <= 0.0:
_choose_new_wander_direction()
_keep_in_bounds()
func _choose_new_wander_direction() -> void:
wander_direction = Vector2(randf_range(-1, 1), randf_range(-1, 1)).normalized()
wander_timer = randf_range(2.0, 5.0)
func _keep_in_bounds() -> void:
var viewport_size: Vector2 = get_viewport().get_visible_rect().size
var margin := 100.0
var turn_force := 2.0
if global_position.x < margin:
wander_direction.x += turn_force * get_physics_process_delta_time()
elif global_position.x > viewport_size.x - margin:
wander_direction.x -= turn_force * get_physics_process_delta_time()
if global_position.y < margin:
wander_direction.y += turn_force * get_physics_process_delta_time()
elif global_position.y > viewport_size.y - margin:
wander_direction.y -= turn_force * get_physics_process_delta_time()
wander_direction = wander_direction.normalized()
func _update_path_prediction() -> void:
if not OS.is_debug_build() or not pathing:
return
if current_state != State.WANDER:
pathing.clear_points()
return
pathing.clear_points()
var sim_pos := Vector2.ZERO
var sim_direction := wander_direction
var step_distance := prediction_length / prediction_steps
for i in prediction_steps:
pathing.add_point(sim_pos)
sim_pos += sim_direction * step_distance
_simulate_boundary_steering(sim_pos, sim_direction)
func _simulate_boundary_steering(sim_pos: Vector2, sim_direction: Vector2) -> void:
var viewport_size := get_viewport_rect().size
var margin := 100.0
var turn_force := 0.5
var world_pos := global_position + sim_pos
if world_pos.x < margin:
sim_direction.x += turn_force
elif world_pos.x > viewport_size.x - margin:
sim_direction.x -= turn_force
if world_pos.y < margin:
sim_direction.y += turn_force
elif world_pos.y > viewport_size.y - margin:
sim_direction.y -= turn_force
sim_direction = sim_direction.normalized()
func _update_visuals() -> void:
if velocity.x != 0:
$Sprite.flip_h = velocity.x < 0
_keep_in_bounds() applies a gentle steering force, when fish approach the edges. This prevents them from leaving the viewport. The _update_path_prediction() function is used to visualize the fish's predicted path, which helps debug the movement.
This looked good at first and fish actually moved around the scene, but Fish would just get stuck and bump into each other not respecting the boundaries of the scene nor each other.
I then decided to add some sort of avoidance behavior and had to tinker around on the script to actually get something "working". I added seperation behavior - when the fish get too close, they calculate a "push away" vector inversely proportional to their distance.
class_name Fish
extends CharacterBody2D
enum State {WANDER, HUNGRY, EATING, FLEEING}
@onready var pathing: Line2D = $Pathing
@export var speed: float = 100.0
@export var prediction_length: float = 200.0
@export var prediction_steps: int = 20
@export var avoidance_radius: float = 60.0
@export var avoidance_strength: float = 3
@export var steering_speed: float = 2.0
var current_state: State = State.WANDER
var wander_direction: Vector2 = Vector2.ZERO
var wander_timer: float = 0.0
var current_direction: Vector2 = Vector2.ZERO
func _ready() -> void:
add_to_group("Fish")
_choose_new_wander_direction()
current_direction = wander_direction
if pathing:
pathing.visible = OS.is_debug_build()
func _physics_process(delta: float) -> void:
match current_state:
State.WANDER:
_handle_wander(delta)
_update_visuals()
if OS.is_debug_build():
_update_path_prediction()
move_and_slide()
func _handle_wander(delta: float) -> void:
var avoidance_direction := _avoid_other_fish()
var direction: Vector2
if avoidance_direction.length() > 0.1:
direction = avoidance_direction
else:
direction = wander_direction
current_direction = current_direction.lerp(direction, steering_speed * delta).normalized()
if current_direction.length_squared() > 0.01:
current_direction = current_direction.normalized()
velocity = current_direction * speed
wander_timer -= delta
if wander_timer <= 0.0:
_choose_new_wander_direction()
_keep_in_bounds()
func _choose_new_wander_direction() -> void:
wander_direction = Vector2(randf_range(-1, 1), randf_range(-1, 1)).normalized()
wander_timer = randf_range(2.0, 5.0)
func _avoid_other_fish() -> Vector2:
var seperation := Vector2.ZERO
var nearby_fish := 0
var all_fish := get_tree().get_nodes_in_group("Fish")
for fish in all_fish:
if fish == self:
continue
var distance := global_position.distance_to(fish.global_position)
if distance < avoidance_radius and distance > 0:
Log.info("Avoiding fish at distance: %s" % distance)
var push_away := global_position.direction_to(fish.global_position) * -1
var strength := 1.0 - (distance / avoidance_radius)
seperation += push_away * strength
nearby_fish += 1
if nearby_fish > 0:
seperation = seperation / nearby_fish
seperation = seperation.normalized() * avoidance_strength
return seperation
func _keep_in_bounds() -> void:
var viewport_size: Vector2 = get_viewport().get_visible_rect().size
var margin := 100.0
var turn_force := 2.0
if global_position.x < margin:
wander_direction.x += turn_force * get_physics_process_delta_time()
elif global_position.x > viewport_size.x - margin:
wander_direction.x -= turn_force * get_physics_process_delta_time()
if global_position.y < margin:
wander_direction.y += turn_force * get_physics_process_delta_time()
elif global_position.y > viewport_size.y - margin:
wander_direction.y -= turn_force * get_physics_process_delta_time()
wander_direction = wander_direction.normalized()
func _update_path_prediction() -> void:
if not OS.is_debug_build() or not pathing:
return
if current_state != State.WANDER:
pathing.clear_points()
return
pathing.clear_points()
var sim_pos := Vector2.ZERO
var sim_direction := wander_direction
var step_distance := prediction_length / prediction_steps
for i in prediction_steps:
pathing.add_point(sim_pos)
sim_pos += sim_direction * step_distance
_simulate_boundary_steering(sim_pos, sim_direction)
func _simulate_boundary_steering(sim_pos: Vector2, sim_direction: Vector2) -> void:
var viewport_size := get_viewport_rect().size
var margin := 100.0
var turn_force := 0.5
var world_pos := global_position + sim_pos
if world_pos.x < margin:
sim_direction.x += turn_force
elif world_pos.x > viewport_size.x - margin:
sim_direction.x -= turn_force
if world_pos.y < margin:
sim_direction.y += turn_force
elif world_pos.y > viewport_size.y - margin:
sim_direction.y -= turn_force
sim_direction = sim_direction.normalized()
func _update_visuals() -> void:
if current_direction.x != 0:
$Sprite.flip_h = current_direction.x < 0
This was a good improvement as the fish now tried avoiding each other. However, another issue occurred: Fish would get stuck in the turning "animation" when bumping into each other or were about to bump into each other. This made it look very funny at first - but it was a bug I certainly have to fix as this will get very frustrating to look at especially when a couple of fish don't know what to do anymore and are stuck in an infinite turning loop while they still get new coordinates to move to.
To fix this issue, I had to rethink how the fish were avoiding each other. The problem was that the avoidance behavior was too strong and would override the wander direction completely, causing fish to get stuck in a loop when multiple fish were trying to avoid each other at the same time.
I decided to implement a smoother steering system that would blend the avoidance direction with the wander direction, rather than replacing it entirely. I also added an alignment behavior that would help fish move in similar directions when they're near each other, which would reduce the chances of them getting stuck in a turning loop.
Here I switched the CharacterBody2D to Area2D because I no longer needed physics-based collision resolution as I'm now handling movement manually and using the area's signals to detect nearby fish more efficiently than iterating through all fish every frame.
class_name Fish
extends Area2D
enum State {WANDER, HUNGRY, EATING, FLEEING}
@onready var pathing: Line2D = $Pathing
@export var speed: float = 100.0
@export var prediction_length: float = 200.0
@export var prediction_steps: int = 20
@export var avoidance_radius: float = 60.0
@export var avoidance_strength: float = 5.0
@export var steering_speed: float = 2.0
@export var alignment_strength: float = 0.5
var velocity: Vector2 = Vector2.ZERO
var current_state: State = State.WANDER
var wander_direction: Vector2 = Vector2.ZERO
var wander_timer: float = 0.0
var current_direction: Vector2 = Vector2.ZERO
var nearby_fish: Array[Fish] = []
func _ready() -> void:
add_to_group("Fish")
_choose_new_wander_direction()
current_direction = wander_direction
area_entered.connect(_on_nearby_fish_entered)
area_exited.connect(_on_nearby_fish_exited)
if pathing:
pathing.visible = OS.is_debug_build()
func _physics_process(delta: float) -> void:
match current_state:
State.WANDER:
_handle_wander(delta)
_update_visuals()
if OS.is_debug_build():
_update_path_prediction()
global_position += velocity * delta
func _handle_wander(delta: float) -> void:
var avoidance := _avoid_other_fish()
var alignment := _align_with_other_fish()
var avoidance_magnitude := avoidance.length()
var wander_weight: float = clamp(1.0 - avoidance_magnitude / avoidance_strength, 0.2, 1.0)
var desired_direction := ((wander_direction * wander_weight) + avoidance + alignment).normalized()
var steer_speed := steering_speed * (1.0 + avoidance_magnitude)
current_direction = current_direction.lerp(desired_direction.normalized(), steer_speed * delta).normalized()
velocity = current_direction * speed
wander_timer -= delta
if wander_timer <= 0.0:
_choose_new_wander_direction()
_keep_in_bounds()
func _choose_new_wander_direction() -> void:
wander_direction = Vector2(randf_range(-1, 1), randf_range(-1, 1)).normalized()
wander_timer = randf_range(2.0, 5.0)
func _avoid_other_fish() -> Vector2:
if nearby_fish.is_empty():
return Vector2.ZERO
var seperation := Vector2.ZERO
for fish in nearby_fish:
if not is_instance_valid(fish):
continue
var to_fish := fish.global_position - global_position
var distance := to_fish.length()
if distance < avoidance_radius and distance > 1.0:
var push_away := -to_fish.normalized()
var strength := 1.0 - (distance / avoidance_radius)
seperation += push_away * strength
Log.info("Fish %s avoidance vector: %s" % [name, seperation])
return seperation.normalized() * avoidance_strength if seperation.length() > 0.1 else Vector2.ZERO
func _align_with_other_fish() -> Vector2:
if nearby_fish.is_empty():
return Vector2.ZERO
var average_direction := Vector2.ZERO
for fish in nearby_fish:
if not is_instance_valid(fish):
continue
average_direction += fish.current_direction
return average_direction.normalized() * alignment_strength if average_direction.length() > 0.01 else Vector2.ZERO
func _keep_in_bounds() -> void:
var viewport_size: Vector2 = get_viewport().get_visible_rect().size
var margin := 100.0
var turn_force := 2.0
if global_position.x < margin:
wander_direction.x += turn_force * get_physics_process_delta_time()
elif global_position.x > viewport_size.x - margin:
wander_direction.x -= turn_force * get_physics_process_delta_time()
if global_position.y < margin:
wander_direction.y += turn_force * get_physics_process_delta_time()
elif global_position.y > viewport_size.y - margin:
wander_direction.y -= turn_force * get_physics_process_delta_time()
wander_direction = wander_direction.normalized()
func _update_path_prediction() -> void:
if not OS.is_debug_build() or not pathing:
return
if current_state != State.WANDER:
pathing.clear_points()
return
pathing.clear_points()
var sim_pos := Vector2.ZERO
var sim_direction := wander_direction
var step_distance := prediction_length / prediction_steps
for i in prediction_steps:
pathing.add_point(sim_pos)
sim_pos += sim_direction * step_distance
_simulate_boundary_steering(sim_pos, sim_direction)
func _simulate_boundary_steering(sim_pos: Vector2, sim_direction: Vector2) -> void:
var viewport_size := get_viewport_rect().size
var margin := 100.0
var turn_force := 0.5
var world_pos := global_position + sim_pos
if world_pos.x < margin:
sim_direction.x += turn_force
elif world_pos.x > viewport_size.x - margin:
sim_direction.x -= turn_force
if world_pos.y < margin:
sim_direction.y += turn_force
elif world_pos.y > viewport_size.y - margin:
sim_direction.y -= turn_force
sim_direction = sim_direction.normalized()
func _update_visuals() -> void:
if current_direction.x != 0:
$Sprite.flip_h = current_direction.x < 0
func _on_nearby_fish_entered(area: Area2D) -> void:
if area is Fish and area != self:
nearby_fish.append(area)
func _on_nearby_fish_exited(area: Area2D) -> void:
if area is Fish:
nearby_fish.erase(area)
I was still not happy with the movement of the fish. Some issues would still occur, like fish getting stuck in corners or not being able to move around the scene properly. I then watched a video on YouTube about someone implementing navigation agents in his game but for player movement avoidance. This got me thinking: if navigation agents could also work for fish movement.
I had to read through the docs about the NavigationAgent2D node and how to actually use it. Navigation agents are actually quite neat as they come with a lot of built-in functionality for pathfinding and avoidance of obstacles. The decision was made to try it out and see, whether it would work for the fish movement.
Adding the NavigationAgent2D node to the fish and setting up the navigation map was quite easy. All I had to do was to create a navigation mesh on the game scene and set it up as the navigation map for the fish. Then I had to actually rewrite 80% of my fish.gd script to make use of the navigation agent.
class_name Fish
extends CharacterBody2D
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
@onready var energy_bar: ProgressBar = $EnergyBar
## Minimum speed of fish
@export var speed_min: float = 10.0
## Maximum speed of fish
@export var speed_max: float = 80.0
# Timer for when fish will pick a new wander target
var wander_timer: float = 0.0
# Current speed of fish
var current_speed: float = 0.0
# Current fish energy
var current_energy: float = 100.0:
set(value):
current_energy = clamp(value, 0, 100)
if energy_bar:
energy_bar.value = current_energy
energy_bar.visible = current_energy < 50
# Timer for how long fish will conserve energy
var conserving_energy_timer: float = 0.0
func _ready() -> void:
add_to_group("fish")
current_speed = randf_range(speed_min, speed_max)
current_energy = randf_range(50, 100)
if energy_bar:
energy_bar.value = current_energy
await get_tree().physics_frame
await get_tree().physics_frame
nav_agent.set_velocity_forced(Vector2.ZERO)
func _on_nav_agent_velocity_computed(suggested_velocity: Vector2) -> void:
velocity = suggested_velocity
move_and_slide()
if velocity.x != 0:
$Sprite.flip_h = velocity.x < 0
To my surprise, the code got simpler - well mostly because I moved most of the code to my state machine. I also decided there, to tinker around with energy management and added a simple energy bar to the fish (for debugging purposes), which would decrease over time and when it reaches 0, the fish would sort of stop getting new coordinates and just sort of "rest" for some time until its energy is restored.
The wandering state now looks like this:
extends FishState
func enter(previous_state_path: String, data := {}) -> void:
_new_wander_location()
func physics_update(_delta: float) -> void:
fish.wander_timer -= _delta
# Consume energy when moving, gain energy when not moving
fish.current_energy -= _delta * 0.1 if fish.velocity.length() > 10 else 0.05
if fish.current_energy < 10:
transitioned.emit(CONSERVING_ENERGY)
return
if fish.wander_timer <= 0.0 or fish.nav_agent.is_navigation_finished():
_new_wander_location()
if not fish.nav_agent.is_navigation_finished():
var next_pos = fish.nav_agent.get_next_path_position()
var dir = (next_pos - fish.global_position).normalized()
fish.nav_agent.set_velocity(dir * fish.current_speed)
func _new_wander_location() -> void:
var vp_size: Vector2 = get_viewport().get_visible_rect().size
var target_position: Vector2 = Vector2(
randf_range(100, vp_size.x - 100),
randf_range(100, vp_size.y - 100)
)
fish.nav_agent.target_position = target_position
fish.wander_timer = randf_range(3.0, 6.0)
fish.current_speed = randf_range(fish.speed_min, fish.speed_max)
I'm happy with the result and the fish movement as it is now much more natural and looks a lot better in general. My state machine also simplified a lot of things and made the code a lot more maintainable.
The approach for the state machine I followed was the Finite State Machine pattern by Nathan Lovato, which also covered a node based implementation - where I'm at right now.
Node based state machine implementation is quite neat and makes the code a lot more readable as each state is a separate node in the scene tree (for me the Fish scene) and a separate script. This makes it easier to understand and maintain the code for me. I also know it might be a bit overkill for a simple game like this, but I thought it's a good learning experience and I would definitely benefit from it in my future games.
The project is still far from being finished but I'm happy with the overall progress I've made so far. I'm not working 24/7 on it but when I do, I feel like making progress, even though I'm somewhat slow at it. What I've learned in November was quite valuable and I'm looking forward to continue the journey on the game.
Some key takeaways from this journey:
With the movement of the fish sorted out, I'm actually now trying to tinker around with some UI elements and the game mechanics like the feeding and the cleaning of the tank and the entire buying update system - which would require to tinker around with a minimal economy system.
The project is open source and can be found on my GitHub profile. You can find the repository here. I'm also happy to receive any feedback or suggestions you might have, as I'm looking to actually improve my understanding of how to make games.
Okay, I think this is enough yapping for now. I'll definitely write more devlogs like these in the future. 👋
2026-01-14 00:20:35
This book is designed as a hybrid learning guide.
Instead of teaching Python by days, it is structured around:
Each chapter builds on the previous one.
You may complete:
All examples, explanations, and code are written for absolute beginners.
How do we communicate clear instructions to a computer?
By the end of this chapter you should be able to:
print() to display outputPython is a programming language you use to tell a computer what to do.
Python is used for:
Python is beginner-friendly because it reads almost like English.
Open your terminal/cmd and type:
python
You’ll see >>> then you can type code directly:
>>> print("Hello")
Hello
To exit:
exit()
.py file
Create a file like day1.py and run:
python day1.py
day1.py
print)
print() does
print() tells Python: show this on the screen.
print("Hello, world!")
print is a function
() means “call/execute the function”"Hello, world!" is a string (text)" " or ' '
Comments help you remember what your code does.
# This is a comment
print("This line will run")
If you come back after 1 week, comments help you understand your own code.
A variable is like a container that stores a value.
Example:
name = "Jane"
age = 17
name is the variable name= means “store this value inside the variable”"Jane" is stored in name
17 is stored in age
business_name = "Snap Pro"
items_in_stock = 25
price = 2500.50
is_open = True
name = "Jane"
print("Hello", name)
Explanation: Python automatically adds a space between items.
name = "Jane"
age = 17
print(f"My name is {name} and I am {age} years old.")
Explanation of f-string
f before the string enables formatting{name} inserts the variable value inside the text✅ Valid:
first_name = "Jane"
age2 = 20
total_sales = 5000
❌ Invalid:
2age = 20 # cannot start with number
my-name = "A" # dash is not allowed
class = "test" # reserved keyword
_
_ for spaces (snake_case)class, def, if)Write a script that prints your:
# Day 1 Project: Personal Profile Script
full_name = "Jane"
business_name = "Snap Pro"
location = "Nigeria"
skill = "Django development"
print("=== MY PROFILE ===")
print("Full name:", full_name)
print("Business name:", business_name)
print("Location:", location)
print("Skill:", skill)
print("\nMessage:")
print(f"Hi, I'm {full_name}. I build digital solutions with {skill}.")
print("=== MY PROFILE ===") prints a title\n means new line (it drops to the next line)Example:
food = "Jollof rice"
app = "Instagram"
country = "Canada"
print(f"My favorite food is {food}.")
print(f"My favorite app is {app}.")
print(f"I would love to visit {country}.")
Make your script look nicer:
----------
print("🔥 Welcome to my profile 🔥")
print("--------------------------")
How do programs collect information from users and work with it?
By the end of this chapter, you will be able to:
input()
input()?
input() allows your program to receive information from the user while it’s running.
Example:
name = input("Enter your name: ")
print("Hello", name)
name
⚠️ Important rule:
input()ALWAYS returns a string
age = input("Enter your age: ")
print(age)
Even if the user types 25, Python sees it as:
"25" ← string, not number
You cannot do math with strings.
❌ This will cause an error:
age = input("Enter age: ")
next_year = age + 1
age = int(input("Enter age: "))
print(age + 1)
| Function | Converts to |
|---|---|
int() |
Whole numbers |
float() |
Decimal numbers |
str() |
Text |
num1 = int(input("Enter first number: "))
num2 = int(input("Enter second number: "))
total = num1 + num2
print("Total:", total)
int() converts them| Operator | Meaning |
|---|---|
+ |
Addition |
- |
Subtraction |
* |
Multiplication |
/ |
Division |
// |
Whole division |
% |
Remainder |
** |
Power |
price = 2500
qty = 3
print(price * qty) # 7500
product = input("Product name: ")
price = float(input("Price per item: "))
quantity = int(input("Quantity: "))
total = price * quantity
print(f"\nProduct: {product}")
print(f"Total cost: ₦{total}")
float() allows decimal pricesint() ensures quantity is a whole numberf-string formats output nicelyBuild a small POS system that:
print("=== SIMPLE POS SYSTEM ===")
product_name = input("Enter product name: ")
price = float(input("Enter product price: "))
quantity = int(input("Enter quantity: "))
total = price * quantity
print("\n--- RECEIPT ---")
print(f"Product: {product_name}")
print(f"Price: ₦{price}")
print(f"Quantity: {quantity}")
print(f"Total Amount: ₦{total}")
print("=== SIMPLE POS SYSTEM ===")
Prints a title.
product_name = input("Enter product name: ")
Receives product name as text.
price = float(input("Enter product price: "))
Converts user input to decimal number.
quantity = int(input("Enter quantity: "))
Converts user input to whole number.
total = price * quantity
Calculates total cost.
print(f"Total Amount: ₦{total}")
Displays final result using f-string.
❌ Forgetting conversion:
price = input("Price: ")
print(price * 2) # wrong
✅ Correct:
price = float(input("Price: "))
print(price * 2)
1️⃣ Ask user for:
2️⃣ Calculate:
year = int(input("Enter year of birth: "))
age = 2026 - year
print(f"You are {age} years old")
3️⃣ Modify POS to include:
Add a 10% discount automatically if total is above ₦20,000.
if total > 20000:
discount = total * 0.10
total -= discount
(Don’t worry if if looks new — you’ll master it on Day 3.)
✔ You can collect real data
✔ You understand data types
✔ You can perform calculations
✔ You've built an interactive program
How does software decide approval, rejection, or status?
By the end of this chapter, you will be able to:
if, elif, and else
Conditions allow your program to make decisions based on data.
Real-life examples:
In Python, we do this using:
if
elif
else
if Statement (Basic Decision)
age = 20
if age >= 18:
print("You are allowed")
age >= 18
⚠️ Indentation matters
if must be indented (usually 4 spaces)if and else (Two Possible Outcomes)
balance = 3000
if balance >= 5000:
print("Transaction approved")
else:
print("Insufficient funds")
if blockelse block| Operator | Meaning |
|---|---|
== |
Equal to |
!= |
Not equal |
> |
Greater than |
< |
Less than |
>= |
Greater or equal |
<= |
Less or equal |
score = 75
if score >= 50:
print("Passed")
else:
print("Failed")
elif (Multiple Conditions)
score = 85
if score >= 80:
print("Grade A")
elif score >= 60:
print("Grade B")
elif score >= 50:
print("Grade C")
else:
print("Failed")
age = int(input("Enter your age: "))
if age >= 18:
print("You are eligible")
else:
print("You are not eligible")
int
amount = float(input("Enter total amount: "))
if amount >= 20000:
discount = amount * 0.10
print(f"Discount applied: ₦{discount}")
else:
print("No discount applied")
Build a program that:
print("=== LOAN ELIGIBILITY CHECKER ===")
income = float(input("Enter your monthly income: ₦"))
if income >= 100000:
print("✅ Loan Approved")
print("You qualify for the loan.")
else:
print("❌ Loan Rejected")
print("Income below required threshold.")
print("=== LOAN ELIGIBILITY CHECKER ===")
Displays program title.
income = float(input("Enter your monthly income: ₦"))
if income >= 100000:
print("✅ Loan Approved")
else:
❌ Using = instead of ==
if income = 100000: # wrong
✅ Correct
if income == 100000:
❌ Forgetting indentation
if income > 50000:
print("Approved") # error
✅ Correct
if income > 50000:
print("Approved")
age = int(input("Enter your age: "))
if age >= 18:
print("You can vote")
else:
print("You cannot vote")
stock = int(input("Enter available stock: "))
if stock > 0:
print("Item available")
else:
print("Out of stock")
score = int(input("Enter your score: "))
if score >= 70:
print("Excellent")
elif score >= 50:
print("Good")
else:
print("Needs improvement")
Build a Payment Status Checker
balance = float(input("Balance: "))
price = float(input("Price: "))
if balance >= price:
print("Payment successful")
else:
print("Insufficient balance")
✔ You can make decisions in code
✔ You understand conditions & comparisons
✔ You can build approval/rejection logic
✔ You're ready for loops
How do we avoid doing the same work repeatedly?
By the end of this chapter, you will be able to:
for loops and while loopsA loop allows Python to repeat an action multiple times.
Instead of writing code again and again, we use loops.
for Loop (When You Know How Many Times)
for i in range(5):
print("Hello")
range(5) means 0 to 4 (5 times)i is a counter (0, 1, 2, 3, 4)range()
| Code | Meaning |
|---|---|
range(5) |
0 → 4 |
range(1, 6) |
1 → 5 |
range(1, 10, 2) |
1, 3, 5, 7, 9 |
for day in range(1, 6):
print("Day", day)
products = ["Rice", "Beans", "Oil"]
for item in products:
print(item)
for day in range(1, 6):
sales = float(input(f"Enter sales for Day {day}: ₦"))
print(f"Sales recorded: ₦{sales}")
while Loop (Repeat Until Condition Is False)
count = 1
while count <= 5:
print("Count:", count)
count += 1
count += 1 prevents infinite loopwhile True:
print("This will run forever")
To stop it manually:
break (Exit a Loop)
while True:
name = input("Enter name (or 'exit'): ")
if name == "exit":
break
print("Hello", name)
exit
continue (Skip One Round)
for num in range(1, 6):
if num == 3:
continue
print(num)
1
2
4
5
Build a program that:
print("=== DAILY SALES RECORDER ===")
total_sales = 0
for day in range(1, 8):
sales = float(input(f"Enter sales for Day {day}: ₦"))
total_sales += sales
print("\nWeekly sales summary")
print(f"Total sales for the week: ₦{total_sales}")
total_sales = 0
Starts total at zero.
for day in range(1, 8):
Loop runs 7 times.
total_sales += sales
Adds each day’s sales to total.
❌ Forgetting to update counter in while
count = 1
while count <= 5:
print(count)
✅ Fix
count += 1
❌ Wrong indentation
for i in range(3):
print(i)
✅ Fix
for i in range(3):
print(i)
for i in range(1, 11):
print(i)
while True:
msg = input("Type something (or 'stop'): ")
if msg == "stop":
break
print(msg)
total = 0
for i in range(5):
num = int(input("Enter number: "))
total += num
print("Total:", total)
Modify the sales recorder:
0
(Hint: use break and a counter)
✔ You can automate repetitive tasks
✔ You understand for and while loops
✔ You can control program flow
✔ You're ready for Lists & Data Collections
How do applications store and manage multiple records?
By the end of this chapter, you will be able to:
A list is a collection of items stored in one place.
Instead of creating many variables:
p1 = "Rice"
p2 = "Beans"
p3 = "Oil"
We use a list:
products = ["Rice", "Beans", "Oil"]
items = ["Bread", "Milk", "Eggs"]
print(items)
[] define a listproducts = ["Rice", "Beans", "Oil"]
print(products[0]) # Rice
print(products[1]) # Beans
print(products[2]) # Oil
Python starts counting from 0, not 1.
products = ["Rice", "Beans", "Oil"]
products[1] = "Garri"
print(products)
1 (Beans) is replaced with Garri
append() (most common)
products = ["Rice", "Beans"]
products.append("Oil")
print(products)
products.remove("Beans")
products.pop()
len())
products = ["Rice", "Beans", "Oil"]
print(len(products)) # 3
products = ["Rice", "Beans", "Oil"]
for item in products:
print(item)
items = []
for i in range(3):
product = input("Enter product name: ")
items.append(product)
print(items)
Build a program that:
print("=== INVENTORY MANAGER ===")
inventory = []
while True:
product = input("Enter product name (or 'exit'): ")
if product == "exit":
break
inventory.append(product)
print("Product added.")
print("\nInventory List:")
for item in inventory:
print("-", item)
print(f"\nTotal products: {len(inventory)}")
inventory = []
Creates an empty list.
while True:
Keeps program running.
inventory.append(product)
Adds each product.
len(inventory)
Counts total items.
❌ Accessing invalid index
print(products[5]) # error if list has only 3 items
✅ Fix
if len(products) > 5:
print(products[5])
❌ Forgetting quotes around strings
products.append(Rice) # wrong
✅ Fix
products.append("Rice")
foods = []
for i in range(3):
food = input("Enter a food: ")
foods.append(food)
for f in foods:
print(f)
items = ["Pen", "Book", "Bag"]
items.remove("Book")
print(items)
names = ["Jane", "John", "Mary"]
print("Total names:", len(names))
Modify Inventory Manager:
if product in inventory:
print("Item already exists")
else:
inventory.append(product)
✔ You can store multiple data values
✔ You can manage dynamic data
✔ You can loop through lists
✔ You're ready for Functions
How do professionals organize code for reuse?
By the end of this chapter, you will be able to:
return
A function is a block of code that does a specific job and can be reused.
Instead of calculating profit again and again:
Think of a function like:
“A machine that takes input → does work → gives output”
Without functions:
With functions:
def)
def greet():
print("Hello, welcome!")
def means define function
greet is the function name() means no input yetgreet()
greet()
Hello, welcome!
Hello, welcome!
def greet_user(name):
print(f"Hello {name}, welcome!")
greet_user("Jane")
greet_user("Chuks")
name is a parameter
def add_numbers(a, b):
print(a + b)
add_numbers(5, 3)
a and b receive valuesreturn (Very Important)
def calculate_profit(cost, selling_price):
profit = selling_price - cost
return profit
result = calculate_profit(500, 800)
print("Profit:", result)
return sends value backreturn does NOT runprint() and return
print() |
return |
|---|---|
| Displays value | Sends value back |
| Cannot be reused | Can be reused |
| For output only | For logic |
❌ Bad practice
def total(a, b):
print(a + b)
✅ Better practice
def total(a, b):
return a + b
def calculate_total():
price = float(input("Price: "))
qty = int(input("Quantity: "))
return price * qty
total = calculate_total()
print("Total:", total)
Build a program that:
print("=== PROFIT CALCULATOR ===")
def calculate_profit(cost_price, selling_price):
return selling_price - cost_price
cost = float(input("Enter cost price: ₦"))
selling = float(input("Enter selling price: ₦"))
profit = calculate_profit(cost, selling)
if profit > 0:
print(f"Profit made: ₦{profit}")
elif profit < 0:
print(f"Loss incurred: ₦{abs(profit)}")
else:
print("No profit, no loss")
def calculate_profit(cost_price, selling_price):
Defines function with two inputs.
return selling_price - cost_price
Calculates and returns result.
profit = calculate_profit(cost, selling)
Stores returned value.
if profit > 0:
Decision based on function result.
❌ Forgetting to call function
calculate_profit
✅ Correct
calculate_profit(500, 700)
❌ Forgetting return
def calc(a, b):
a + b
✅ Correct
def calc(a, b):
return a + b
def welcome(name):
print(f"Welcome {name}")
welcome("Jane")
def area(length, width):
return length * width
print(area(5, 4))
def square(num):
return num * num
print(square(2))
print(square(5))
Create a Simple POS Function
def apply_discount(amount):
if amount > 20000:
return amount * 0.9
return amount
✔ You can write clean reusable code
✔ You understand functions & return values
✔ You can separate logic from output
✔ You're ready for Building a Complete Python Application
How do we combine all Python fundamentals into a real system?
By the end of this chapter, you will:
| Chapter | Concept |
|---|---|
| Chapter 1 | Variables, print |
| Chapter 2 | input(), numbers |
| Chapter 3 | if / else |
| Chapter 4 | loops |
| Chapter 5 | lists |
| Chapter 6 | functions |
Build a program that:
This is similar to:
def calculate_total(price, quantity):
return price * quantity
price and quantity
print("=== SIMPLE SALES MANAGEMENT SYSTEM ===")
sales = [] # list to store sales
grand_total = 0 # total sales amount
def calculate_total(price, quantity):
return price * quantity
while True:
product = input("\nEnter product name (or 'exit'): ")
if product.lower() == "exit":
break
price = float(input("Enter price: ₦"))
quantity = int(input("Enter quantity: "))
total = calculate_total(price, quantity)
grand_total += total
sales.append(product)
print(f"Total for {product}: ₦{total}")
print("\n=== SALES SUMMARY ===")
for item in sales:
print("-", item)
print(f"\nNumber of products sold: {len(sales)}")
print(f"Grand Total Sales: ₦{grand_total}")
print("=== SIMPLE SALES MANAGEMENT SYSTEM ===")
Displays program heading.
sales = []
grand_total = 0
sales stores product namesgrand_total accumulates total salesdef calculate_total(price, quantity):
return price * quantity
Handles calculation logic cleanly.
while True:
Keeps program running until user stops it.
if product.lower() == "exit":
break
.lower() makes input case-insensitiveprice = float(input("Enter price: ₦"))
quantity = int(input("Enter quantity: "))
Gets numeric input.
total = calculate_total(price, quantity)
Uses function.
sales.append(product)
grand_total += total
Stores data and updates total.
print("\n=== SALES SUMMARY ===")
Prints results neatly.
❌ Forgetting to convert input
price = input("Price: ") # wrong
✅ Correct
price = float(input("Price: "))
❌ Not using break
while True:
print("Running forever")
sales.append((product, total))
if product == "":
print("Product name cannot be empty")
continue
if total > 20000:
total *= 0.9
Upgrade the app to:
Example:
Rice - Qty: 2 - ₦5000
Beans - Qty: 1 - ₦3000
After completing this book, you can:
✔ Write clean Python scripts
✔ Build interactive programs
✔ Use logic, loops, lists, and functions
✔ Create real-life tools for business
✔ Move confidently into Django, automation, or AI
You have completed *Python Fundamentals *.
2026-01-14 00:19:45
I've been using Payload CMS a lot, and I believe it's becoming a pretty hot emerging tech in the Next.js space. As someone who's used to building APIs in Express and developing admin dashboards in React from scratch, it gets pretty tedious when basic APIs and logic (like CRUD-ing a table or handling login screens) get repeated over and over.
Sure, I've used Prisma, Drizzle, tRPC, admin dashboard templates, and the good ol' CTRL+C CTRL+V, but still having to separately handle the backend and frontend for a feature felt like it could be easier. So you should see my face when I first tried out Payload CMS with its config-based approach of building the frontend, backend, and even the data layer of an app. It's so much quicker to declare a Payload collection, which is then generated as a frontend in the admin dashboard, backend APIs, and Drizzle schemas with TypeScript interfaces for me to use instantly. This process would've taken me hours or even days if I were to use my good ol' MERN stack, but with Payload its just instant!
I have no qualms with Payload's DX (though the documentation for advanced use-cases does leave much to be desired), but I'm starting to feel friction when working on a large Payload project with about 20+ collections. My biggest Payload project, Akasia Education, has about 30+ collections with some of them having complex relationships/join fields to multiple other collections. Having an ERD does help, but it's hard for me to actually visualize how it's implemented in Payload CMS. I would have to read each payload collection individually, see their relationship/join field, and imagine it in my head if it fits with my ERD or not. I even encountered errors that were hard to debug due to my collections having multiple relationships with other collections, where those collections have relationships with other collections, and so forth.
Usually, finding where a piece of variable/function/class is used on a project is easy in an IDE with features like go-to definitions or find all references. But finding where a Payload collection is referenced by other collections is more difficult since relationship/join fields use another collection's slug, which are just strings that cannot be used for go-to definitions/find all references in IDEs. So, being inspired by Code Canvas extension and always wanting to make a VSCode extension on my own, is it possible to build a Payload CMS collection visualizer where I can simply see how each collection relates to one another?
It's super simple: get all Payload collection in a project, analyze their relationship, and visualize them in a canvas like above. Having something like this would really help me visualize my big Payload project without having to take out a pen-and-pencil or always compare it againts my ERD.
With nothing else to do in my free time, I just started building this extension idea. The main idea is that my extension can be activated on a Payload CMS project, and it will traverse the collections to build a visualizer to show how each collection relates to the others. Basically, I want to make something like Code Canvas, but for Payload collections.
Obivously the first thing I need to ensure is if it's even possible. Well, if Code Canvas is possible, then of course this idea should also be. Specifically, I need to make sure how I would implement this and what stack I need.
Reading VSCode extension development docs, it is possible by making a webview-based extension. Frankly, there's not much structured resource on how to build a webview VSCode extension, but after some digging, I found 2 good repositories to serve as a boilerplate: Microsoft's official example and QiyuanChen02 repo.
Ultimately, I chose QiyuanChen02's repo since its newer, but these 2 repo provides a boilerplate for VSCode webview extension using React + Vite and hot-reload. The gist of the development flow is I just build a normal React app whose built code will be rendered as a webview in VSCode, and I can pass messages between VSCode and my webview using message passing. Next is how to render the visualizer. I chose React Flow since it's super popular for this.
Now that the basic prerequisite is possible for me to build the extension, it's time to actually build the extension and not abandon it as another side project.
I forked QiyuanChen02's repo to quickly bootstrap my extension project here: https://github.com/ElysMaldov/payload-cms-booster. I called it Payload CMS Booster because I have a few other ideas for this extension to boost working with Payload in VSCode. It just works by running the debug mode (F5), it will open up another VSCode window with my custom command to show the React app in a webview. I added a new launch JSON that starts the debug VSCode window in a clean slate by disabling all other extensions except the one I'm developing and also opening an existing Payload project to test it later; I placed this new debug config in the launch.json.
For the folder structure, it's quite straightforward. I have a "root" project with the main entrypoint being the src/extension.ts. This is where I register my webview to the extension. The webview takes in the dist folder of the React webview subproject in the webview folder. I have 2 projects, each having its own dependencies. For now, this works, but I could potentially turn it into a monorepo like how Kilo Code does it.
To run the project, I need to run the dev server in the webview project and start a debugger to open up another VSCode window. In that new window, I can access the command to bring up the webview (currently the command is "Payload CMS Booster: Visualize" specified in the root package.json or VSCode calls it extension manifest). Then I can just work on the webview project like a regular React + Vite project with HMR working.
The latest progress I made with this repo is setting up an example React Flow component. I haven't really thought much about how to architect this project but I'll try applying parts of MVVM since I've been working on a lot of Flutter these days; mainly I need to separate between the data and UI layer, and use domain models a lot so I don't have to worry much about how to pass my actual payload collection data from VSCode since I can just assume that's a DTO I need to map to my domain model.
So my next step is how can I traverse all of the Payload collection in a project, analyze their relationships with each other, and structure it for my webview and finally render it in React Flow? The idea I have so far is to read the payload.config.ts, iterate through each collection to collect their slug, and build their relationship based on the existence of either a relationship/join field in that collection. This seems okay so far since a relationship/join field refers to another collection using their unique slug, so I could use this as an id for each collection. I thought about using the generated payload-types.ts file, but it seems wayyyy more complicated than a regular Payload collection file.
I'll update again soon 😸.
2026-01-14 00:13:15
I recently built SS7 Store, an e-commerce platform designed to test and validate complete shopping and payment workflows.
Tech Stack
Next.js
TypeScript
Supabase
Stripe (test payment integration)
Implemented Features
Product listing
Add to cart functionality
Buy now flow with full order tracking
Bank payment method
Note: This project is created for survey and functionality testing purposes only and is not intended for production use.
This build helped me explore real-world e-commerce logic, payment flows, and backend integration using Supabase.
Feedback and suggestions are welcome 🙌