2026-03-02 11:42:59
This is a submission for the DEV Weekend Challenge: Community
"Uganda may not have launched a rocket yet, but we've launched something just as important: interest." 🚀
Space Junkies Uganda (SJU) was born out of deep love for space, for Uganda, and for the belief that the cosmos belongs to everyone, including us.
I'm Ronnie Atuhaire, and I've been procrastinating on building our web presence since we started. But this challenge? This was the push I needed.
The Origin Story:
Last year, I was approached to serve as a judge for the NASA Space Apps Challenge in Kampala. During that event, I witnessed something powerful: students weren't just interested in space, they were passionate about it. Their eyes lit up when talking about black holes, Mars missions, and Uganda's own satellite, PearlAfricaSat-1 (launched in 2022!).
Around the same time, I became the temporary National Coordinator for World Space Week in Uganda. If you're into space, you know October is the month. So we went all in.
Uganda Space Week 2025 (October 8-10) was our first national celebration of space and astronomy, held at Makerere University. Over three days:
More info 👇
During our stargazing session at Kololo Hill, I realized: this wasn't just an event. It was a connection between imagination and science, between curiosity and community.
Why Uganda?
We sit on the equator (0.3136°N, 32.5811°E) - one of the best places on Earth for astronomical observations. We have PearlAfricaSat-1 orbiting above us. We have passionate students, amateur astronomers, and engineers who dream big.
What we didn't have was a digital home. A place to coordinate stargazing nights, share telescope observations, organize dark sky expeditions to Lake Mburo, and prove that African space enthusiasts are already here, already building.
Space Junkies Uganda is now 135+ members strong, and this platform is our mission control.
A full-stack community platform featuring:
For Members:
For Admins:
Try it: https://spacejunkies-production.up.railway.app/
Design Philosophy:
Cyberpunk-meets-cosmos aesthetic with neon orange (#FF4500), cyan (#00FFD1), and deep space blacks. Terminal-style fonts (Orbitron, Share Tech Mono), scanline overlays, aurora effects, and floating particles create an immersive "space command center" vibe. Think Blade Runner meets the ISS.
🚀 Live Features:
/admin.html
Key Interactions:
Here is the GitHub Repo: https://github.com/Ronlin1/space_junkies
Architecture:
space_junkies/
├── server.js # Express server + Gemini AI proxy
├── public/
│ ├── index.html # Single-page app (all pages in one file)
│ ├── admin.html # Admin panel
│ ├── favicon.jpg # Site icon
│ └── uploads/ # User-uploaded gallery images
├── gallery-data.json # Persistent gallery storage
├── members-data.json # Member requests storage
├── events-data.json # Events storage
├── .env # GEMINI_API_KEY
└── package.json # Dependencies
Tech Stack:
@google/genai SDK/uploads/)Key Features Implementation:
// Server: Upload endpoint
app.post('/api/admin/gallery/upload', upload.array('images', 10), (req, res) => {
const uploadedImages = req.files.map(file => ({
id: Date.now() + '-' + Math.random().toString(36).substring(2, 11),
url: `/uploads/${file.filename}`,
caption: req.body.caption || '',
uploadedAt: new Date().toISOString()
}));
galleryImages.push(...uploadedImages);
saveData(GALLERY_FILE, galleryImages);
res.json({ success: true, images: uploadedImages });
});
// Client: Load and display
async function loadGalleryImages(){
const res = await fetch('/api/gallery');
const data = await res.json();
data.images.forEach((img, index) => {
placeholders[index].innerHTML = `
<img src="${img.url}" alt="${img.caption}">
<div class="gal-overlay">
<div class="gal-label">${img.caption}</div>
</div>
`;
placeholders[index].onclick = () => openLightbox(index);
});
}
function openLightbox(index){
currentLightboxIndex = index;
updateLightboxImage();
document.getElementById('lightbox').classList.add('active');
document.body.style.overflow = 'hidden';
}
function changeLightboxImage(direction){
currentLightboxIndex += direction;
if(currentLightboxIndex < 0) currentLightboxIndex = galleryImagesData.length - 1;
if(currentLightboxIndex >= galleryImagesData.length) currentLightboxIndex = 0;
updateLightboxImage();
}
// Keyboard navigation
document.addEventListener('keydown', e => {
if(!document.getElementById('lightbox').classList.contains('active')) return;
if(e.key === 'Escape') closeLightbox();
if(e.key === 'ArrowLeft') changeLightboxImage(-1);
if(e.key === 'ArrowRight') changeLightboxImage(1);
});
const { GoogleGenAI } = require('@google/genai');
app.post('/api/chat', async (req, res) => {
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const response = await ai.models.generateContent({
model: "gemini-3.1-pro-preview",
contents: `${SYSTEM_PROMPT}\n\nUser: ${req.body.message}`
});
res.json({ reply: response.text });
});
requestAnimationFrame
The Journey:
I started ideating with Claude Code Opus, exploring the concept of a space community platform for Uganda. The vision was clear: create something that felt like a NASA mission control center but accessible to everyone.
Moved to Manus to prototype the UI/UX, experimenting with the cyberpunk aesthetic-neon colors, terminal fonts, scanline effects. The design language emerged: "What if Blade Runner met the ISS?"
Finally settled with Kiro and Copilot CLI for the heavy lifting. Kiro's autonomous coding capabilities were perfect for scaffolding the entire single-page app structure, implementing the game logic, and building the admin panel. Copilot CLI accelerated the API integrations and helped debug the trickier parts (like fixing the duplicate id="events" issue that broke navigation).
Development Highlights:
Single-Page Architecture - All pages in one HTML file, JavaScript handles routing. Keeps deployment simple and load times instant.
No Database - JSON files for persistence. For a community of 135 members, this is perfect. Simple, version-controllable, no setup required.
Gemini Integration - Switched from fetch-based API to the official @google/genai SDK. Much cleaner error handling and automatic retries.
Gallery Upload Flow - The trickiest part was ensuring uploads persist AND display on the main site. Solution: Server saves to JSON + filesystem, client fetches on page load and replaces placeholders sequentially.
Lightbox Slider - Built from scratch with CSS animations and keyboard support. No libraries needed. Smooth zoom-in effect using cubic-bezier(.34,1.56,.64,1) for that satisfying bounce.
Game Development - Canvas API for rendering, collision detection math, health system, HUD overlay. The exit button was crucial; players needed an escape hatch without losing progress.
Events Page Fix - Duplicate IDs (id="events" on both home section and events page) broke navigation. Changed home section to id="home-events", added missing CSS for calendar and event cards.
Favicon - Simple but important. Added <link rel="icon" type="image/jpeg" href="/favicon.jpg"> to complete the professional look.
Challenges Overcome:
taskkill /F /IM node.exe before starting.app.get('*', ...) BEFORE API endpoints, intercepting all requests. Moved to end of file.uploads/ folder auto-creates with fs.mkdirSync(uploadDir, { recursive: true }).Technologies:
Deployment Ready:
.env
/admin.html
/health
What's Next:
The Real Mission:
This platform isn't just about code. It's about proving that space exploration starts with curiosity, not budgets. It's about showing that a kid in Kampala can look up at the same stars as a kid in Cape Canaveral and dream just as big.
During Uganda Space Week, I operated a telescope for the first time. I pointed it the wrong way initially, but when we finally locked onto Jupiter, seeing those Galilean moons with my own eyes, that moment changed everything. That's what this platform is for: creating those moments for others.
We may not have $12 million spacesuits, but we have something better: a community that believes the next frontier of discovery starts right here in Uganda. 🇺🇬
Built with ❤️ for Space Junkies Uganda
Founded by Ronnie Atuhaire • January 2025
We're already gone. 🚀
Special thanks to all IEEE core members, our amazing partners, and every space enthusiast who made Uganda Space Week possible. You are the real stars. 🌠
2026-03-02 11:41:02
This article explains Firebase Remote Config (real-time updates, feature flags, A/B testing, Compose integration).
// build.gradle.kts
dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
implementation("com.google.firebase:firebase-config-ktx")
}
<!-- res/xml/remote_config_defaults.xml -->
<?xml version="1.0" encoding="utf-8"?>
<defaultsMap>
<entry>
<key>feature_new_ui</key>
<value>false</value>
</entry>
<entry>
<key>welcome_message</key>
<value>Welcome!</value>
</entry>
<entry>
<key>max_items</key>
<value>20</value>
</entry>
</defaultsMap>
class RemoteConfigRepository @Inject constructor() {
private val remoteConfig = Firebase.remoteConfig.apply {
setConfigSettingsAsync(remoteConfigSettings {
minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 3600
})
setDefaultsAsync(R.xml.remote_config_defaults)
}
private val _configUpdates = MutableSharedFlow<Unit>(replay = 1)
init {
remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
override fun onUpdate(configUpdate: ConfigUpdate) {
remoteConfig.activate().addOnCompleteListener {
_configUpdates.tryEmit(Unit)
}
}
override fun onError(error: FirebaseRemoteConfigException) {
Log.e("RemoteConfig", "Update error", error)
}
})
fetchAndActivate()
}
private fun fetchAndActivate() {
remoteConfig.fetchAndActivate()
}
fun getBoolean(key: String): Boolean = remoteConfig.getBoolean(key)
fun getString(key: String): String = remoteConfig.getString(key)
fun getLong(key: String): Long = remoteConfig.getLong(key)
val updates: SharedFlow<Unit> = _configUpdates
}
@HiltViewModel
class MainViewModel @Inject constructor(
private val remoteConfig: RemoteConfigRepository
) : ViewModel() {
val isNewUiEnabled: StateFlow<Boolean> = remoteConfig.updates
.map { remoteConfig.getBoolean("feature_new_ui") }
.stateIn(viewModelScope, SharingStarted.Eagerly, remoteConfig.getBoolean("feature_new_ui"))
val welcomeMessage: StateFlow<String> = remoteConfig.updates
.map { remoteConfig.getString("welcome_message") }
.stateIn(viewModelScope, SharingStarted.Eagerly, remoteConfig.getString("welcome_message"))
}
@Composable
fun HomeScreen(viewModel: MainViewModel = hiltViewModel()) {
val isNewUi by viewModel.isNewUiEnabled.collectAsStateWithLifecycle()
val welcomeMessage by viewModel.welcomeMessage.collectAsStateWithLifecycle()
Column(Modifier.padding(16.dp)) {
Text(welcomeMessage, style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(16.dp))
if (isNewUi) {
NewFeatureCard()
} else {
LegacyContent()
}
}
}
@Composable
fun NewFeatureCard() {
Card(Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp)) {
Text("New Feature", style = MaterialTheme.typography.titleMedium)
Text("This new UI is enabled via Remote Config")
}
}
}
| Feature | Implementation |
|---|---|
| Initialization | Firebase.remoteConfig |
| Default values | setDefaultsAsync() |
| Real-time | addOnConfigUpdateListener |
| Feature flags | getBoolean("key") |
| A/B testing | Firebase Console configuration |
fetchAndActivate() to get latest valuesConfigUpdateListener reflects changes in real-time8 production-ready Android app templates (Firebase integrated) are available.
Browse templates → Gumroad
Related articles:
8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.
Browse templates → Gumroad
2026-03-02 11:40:30
Retrofit and OkHttp power network operations in Android apps. Master API definitions, custom interceptors, and token-based authentication.
Create type-safe API endpoints:
interface ApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") userId: String): User
@POST("posts")
suspend fun createPost(@Body post: PostRequest): PostResponse
@GET("posts")
suspend fun listPosts(
@Query("page") page: Int = 1,
@Query("limit") limit: Int = 20
): List<Post>
}
Configure HTTP client with interceptors:
val httpClient = OkHttpClient.Builder()
.addInterceptor(LoggingInterceptor())
.addNetworkInterceptor(StethoInterceptor())
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(httpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build()
val apiService = retrofit.create(ApiService::class.java)
Automatically attach tokens to requests:
class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val token = tokenProvider.getAuthToken()
val authenticatedRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer $token")
.build()
return chain.proceed(authenticatedRequest)
}
}
// Add to OkHttpClient
httpClient.addInterceptor(AuthInterceptor(tokenProvider))
Implement robust error handling:
class ErrorInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var response = chain.proceed(chain.request())
if (response.code == 401) {
// Token expired, refresh and retry
refreshToken()
response = chain.proceed(chain.request())
}
return response
}
}
// Use with coroutines
viewModelScope.launch {
try {
val user = apiService.getUser("123")
updateUI(user)
} catch (e: HttpException) {
showError("HTTP Error: ${e.code()}")
} catch (e: IOException) {
showError("Network Error")
}
}
Retrofit simplifies API integration. Combine it with proper interceptor setup for production-ready networking.
8 Android app templates on Gumroad
2026-03-02 11:40:25
This article explains Compose stability (@stable, @Immutable, skippable judgment, Compose Compiler Report, and performance optimization).
Compose skips recomposition if arguments haven't changed. Whether skipping is possible depends on the "stability" of the arguments.
// ✅ Stable (auto-detected): primitives, String, function types
@Composable
fun Greeting(name: String) { // String = stable → skippable
Text("Hello, $name")
}
// ❌ Unstable: List, Map and other collections
@Composable
fun UserList(users: List<User>) { // List = unstable → recomposed every time
LazyColumn {
items(users) { UserItem(it) }
}
}
@Immutable
data class User(
val id: String,
val name: String,
val email: String
)
// If all properties are val and stable types, add @Immutable
// Compose treats this class as guaranteed immutable
@Stable
class CounterState {
var count by mutableIntStateOf(0)
private set
fun increment() { count++ }
}
// @Stable = "As long as equals() returns the same result for the same instance, skipping is possible"
// Use on classes containing MutableState
// Method 1: kotlinx.collections.immutable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
fun UserList(users: ImmutableList<User>) { // ✅ Stable
LazyColumn {
items(users.size) { index -> UserItem(users[index]) }
}
}
// Caller side
val users = usersList.toImmutableList()
// Method 2: Wrapper class
@Immutable
data class UserListWrapper(val users: List<User>)
// build.gradle.kts
android {
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_reports")
metricsDestination = layout.buildDirectory.dir("compose_metrics")
}
}
// Example report output
// app_release-composables.txt
restartable skippable scheme("[androidx.compose.ui.UnitComposable]") fun Greeting(
stable name: String
)
restartable scheme("[androidx.compose.ui.UnitComposable]") fun UserList(
unstable users: List<User> // ← Unstable! Not skippable
)
@Composable
fun FilteredList(items: List<Item>, query: String) {
// ❌ Recomputed every time
val filtered = items.filter { it.name.contains(query) }
// ✅ Recomputed only when result changes
val filtered by remember(items, query) {
derivedStateOf { items.filter { it.name.contains(query) } }
}
LazyColumn {
items(filtered) { item -> ItemRow(item) }
}
}
@Composable
fun ItemList(items: List<Item>) {
LazyColumn {
items(
items = items,
key = { it.id } // ✅ key guarantees identity → reused
) { item ->
ItemRow(item)
}
}
}
// ❌ New lambda instance created every time
@Composable
fun Parent(viewModel: MyViewModel) {
Child(onClick = { viewModel.doSomething() })
}
// ✅ remember stabilizes lambda
@Composable
fun Parent(viewModel: MyViewModel) {
val onClick = remember(viewModel) { { viewModel.doSomething() } }
Child(onClick = onClick)
}
| Technique | Use Case |
|---|---|
@Immutable |
Immutable data classes |
@Stable |
Classes with MutableState |
ImmutableList |
Stabilize collections |
derivedStateOf |
Optimize derived state |
key |
LazyList item identity |
| Compiler Report | Discover unstable args |
@Immutable/@Stable
kotlinx.collections.immutable
derivedStateOf
8 production-ready Android app templates (performance optimized) are available.
Browse templates → Gumroad
Related articles:
8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.
Browse templates → Gumroad
2026-03-02 11:37:45
You’ve seen it before: an app starts failing with "sorry, too many clients already", and you only notice when users complain. By then, the database is saturated, and even your admin tools can’t connect. pgwd (Postgres Watch Dog) is a small Go CLI that watches your connection counts and alerts you before you hit the limit—and when you can’t connect at all.
PostgreSQL has a max_connections limit. When you exceed it:
superuser_reserved_connections.Without something watching connection usage, you only find out when things are already broken.

How pgwd fits in: it watches your Postgres and pushes alerts to Slack and/or Loki when thresholds are exceeded or the connection fails.
pgwd connects to your Postgres (directly or via Kubernetes), reads connection stats from pg_stat_activity, and sends alerts to Slack and/or Loki when:
max_connections).So you get notified when you’re approaching the limit, and again when the instance is already saturated.

When the DB is saturated, pgwd sends an urgent alert like this to your notifiers.
# Alert when total connections >= 80% of server max_connections (default)
pgwd -db-url "postgres://user:pass@localhost:5432/mydb" \
-slack-webhook "https://hooks.slack.com/services/..."
No need to set thresholds if you’re fine with 80%: pgwd reads max_connections from the server and applies a default percentage.
export PGWD_DB_URL="postgres://localhost:5432/mydb"
export PGWD_SLACK_WEBHOOK="https://hooks.slack.com/..."
export PGWD_INTERVAL=60
pgwd
# Runs every 60 seconds; exit with SIGTERM.
pgwd -db-url "postgres://..." -slack-webhook "https://..." \
-stale-age 600 -threshold-stale 1
Alerts when any connection has been open longer than 10 minutes—handy for spotting leaks or long-running transactions.
pgwd -kube-postgres default/svc/postgres \
-db-url "postgres://user:DISCOVER_MY_PASSWORD@localhost:5432/mydb" \
-slack-webhook "https://..."
pgwd runs kubectl port-forward, can read the password from the pod’s environment, and connects to localhost. Alerts include cluster/namespace/service context.
If the database is unreachable or returns "too many clients", pgwd always sends an urgent alert to your notifiers (when configured)—no extra flag needed. So even in the worst case, you get a Slack/Loki message instead of silence.
go install github.com/hrodrig/pgwd@latest
ghcr.io/hrodrig/pgwd)One-shot, daemon, or cron—with Slack and/or Loki you can stop flying blind on connection usage and get ahead of "too many clients" before it hits production.
Disclosure: This post was drafted with AI assistance and reviewed by the author.
2026-03-02 11:35:43
AI agents are making us incredibly fast, but they're also making it dangerously easy to ship insecure code.
Students and junior devs blindly follow code suggestions from Copilot and ChatGPT, not even once thinking about the structural integrity of the code. They may or may not have SQL injections, exposed API keys, and severe architectural flaws.
We don't need AI to stop writing code for us. We need better tools to understand and verify what the AI wrote before it hits production.
Soteria is an AI-powered code security platform built specifically for students and early-career developers. Think of it as an educational firewall. It doesn't just highlight vulnerabilities; it helps you build a mental model for secure coding.
Built to recognize 50+ languages, Soteria instantly detects injection flaws, XSS, and dozens of other vulnerability patterns.
Not aware of the problem at hand? No problem, every bug/vulnerability has a beginner-friendly, plain-English explanation of why the code is dangerous and exactly how to fix it.
The better you get at your security habits, the more XP you earn for every scan, level up your rank (from Novice to Architect), and build your security intuition over time.
We leverage the Gemini 2.5 Pro API for deep contextual analysis of code snippets. Instead of just running static regex checks, Soteria passes the code context to Gemini, which acts as the "Neural Engine" to explain why something is vulnerable and provide exact, context-aware remediation steps.
We also included a structured architecture with specialized engines (like the KnowledgeGraph and NeuralEngine) to parse API responses, structure the vulnerability data, and ensure the explanations are accurate and beginner-friendly.
The gap between "it works" and "it's secure" is massive. I wanted to create a tool that catches developers right at the moment of integration.
If we can teach developers to recognize a vulnerable pattern specifically when they are about to commit it, the amount of money and time saved is astronomical!
But for all this to happen, I need YOUR guys' help‼️
Sometimes developers have an oversight with their code (with bugs and features that could be a game-changer), and that's where users come in 👀.
SO GO TRY IT: trysoteria.live
Leave a comment down below or connect with me on LinkedIn