Tu Cielo de Noche
Jorge Delgadillo

#website link Tu Cielo de Noche

Mapping the Cosmos: Building a Microservice-Driven Celestial Map Generator

Introduction

Ever looked up at the night sky and wondered what the stars looked like on a specific date and time from a particular location? That's the question that sparked the creation of "Tu Cielo de Noche," a web application that transforms celestial data into personalized, printable star maps. This project wasn't just about pretty pictures; it was a deep dive into microservices, serverless architecture, and the intricacies of astronomical calculations. Join me as I share the journey of building this unique e-commerce experience.

The Vision: Personalized Celestial Maps

"Tu Cielo de Noche" empowers users to generate a stunning stereographic representation of the night sky, capturing the precise positions of stars, planets, constellations, the moon's phase, and the sun, all based on a user-defined date, time, and location. The core of this application lies in its ability to translate complex astronomical data from the Hipparcos catalog (stored as a JSON file) into a visual masterpiece using the powerful astronomy.js library for the calculations.

Tech Stack: A Microservices Approach

To bring this vision to life, I assembled a robust tech stack:

The Challenge: Consistent Image Rendering

One of the biggest hurdles was ensuring consistent image quality and dimensions across all orders. Client-side rendering, while convenient, was limited by the user's screen resolution and DPI. To overcome this, I implemented a dual-rendering approach:

This meant duplicating the celestial map processing logic, but it was essential for maintaining quality.

The Workflow: Payment-Triggered Processing

To optimize resource usage and ensure that map generation only occurred for successful orders, I integrated Stripe webhooks. When a payment was successful, Stripe triggered a webhook event, which in turn placed a message containing the order ID and celestial map configuration onto an AWS SQS queue.

Here's a simplified example of how the Stripe webhook processes successful payment events:

// Simplified Stripe Webhook Example

import Stripe from "stripe";

// Initialize Stripe (replace with your test secret key for example)
const stripe = new Stripe("sk_test_example", {
  apiVersion: "2023-10-16",
});

// Example webhook secret (replace with a test secret)
const webhookSecret = "whsec_example";

export async function handler(req: Request) {
  try {
    const body = await req.text();
    const signature = req.headers.get("stripe-signature");

    if (!signature) {
      console.error("No Stripe signature found.");
      return new Response(JSON.stringify({ error: "No signature" }), {
        status: 200,
      });
    }

    let event: Stripe.Event;
    try {
      event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
    } catch (err) {
      console.error("Webhook signature verification failed:", err);
      return new Response(JSON.stringify({ error: "Verification failed" }), {
        status: 200,
      });
    }

    if (event.type === "payment_intent.succeeded") {
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      const orderId = paymentIntent.metadata?.orderId;

      if (orderId) {
        console.log(`Payment succeeded for order: ${orderId}`);
        // Simulate adding job to queue (replace with your queue logic)
        await addToQueue(orderId, paymentIntent.metadata);
      } else {
        console.error("No orderId found in payment intent metadata.");
      }
    }

    return new Response(JSON.stringify({ received: true }), { status: 200 });
  } catch (err) {
    console.error("Webhook processing error:", err);
    return new Response(JSON.stringify({ error: "Processing error" }), {
      status: 200,
    });
  }
}

// Example function to simulate adding a job to a queue
async function addToQueue(orderId: string, metadata: any) {
  console.log(
    `Simulating adding order ${orderId} to queue with metadata:`,
    metadata
  );
  // Replace this with your actual queue integration logic.
  await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate queue delay
  console.log(`Order ${orderId} added to queue.`);
}

An AWS Lambda function, acting as a worker, then consumed these messages and executed the server-side celestial map processor. This ensured that map generation was tightly coupled with payment success.

The Lambda Function: Processing SQS Events

The core of our server-side processing is an AWS Lambda function that handles messages from the SQS queue. This function is triggered whenever a new message arrives, signaling a successful payment and the need to generate a celestial map.

Here's a simplified example of the Lambda function's core logic:

// Simplified Lambda Function Example

import { SQSEvent, Context } from "aws-lambda";

/**
 * Example function to process SQS messages.
 * This simulates the core logic of your celestial map processing.
 */
export async function handler(
  event: SQSEvent,
  context: Context
): Promise<void> {
  console.log("Processing SQS event...");

  // Iterate through each record in the SQS event
  for (const record of event.Records) {
    try {
      // Parse the JSON message body
      const messageBody = JSON.parse(record.body);

      // Extract order ID and celestial map configuration
      const orderId = messageBody.orderId;
      const mapConfig = messageBody.mapConfig;

      console.log(`Processing order ID: ${orderId}`);

      // Simulate celestial map processing (replace with your actual logic)
      await processCelestialMap(orderId, mapConfig);

      console.log(`Order ID: ${orderId} processed successfully.`);
    } catch (error) {
      console.error("Error processing SQS message:", error);
    }
  }

  console.log("SQS event processing complete.");
}

/**
 * Example function to simulate celestial map processing.
 * Replace this with your actual map generation logic.
 */
async function processCelestialMap(
  orderId: string,
  mapConfig: any
): Promise<void> {
  // Simulate asynchronous map generation
  await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate 1 second processing time

  console.log(
    `Simulated celestial map generation for order ID: ${orderId} with config:`,
    mapConfig
  );
  // Here, you would use mapConfig to generate the celestial map.
  // And then store the result.
}

The Lambda Deployment Saga: Docker and Manifest Compatibility

Deploying the server-side celestial map processor as an AWS Lambda function presented a unique challenge. The canvas.js library, with its native dependencies, required a Docker image container for deployment.

However, I encountered a frustrating error: "manifest not compatible." After some research, I discovered that AWS Lambda had limitations with multi-architecture Docker image manifests. The solution was simple but crucial:

docker build --platform linux/amd64 -t celestial-map-processor:latest

By explicitly specifying the linux/amd64 platform during the Docker build process, I resolved the manifest compatibility issue. This was a significant win, and it felt incredibly rewarding to overcome this technical hurdle.

The Outcome: A Seamless Celestial Experience

With the deployment issues resolved, "Tu Cielo de Noche" began functioning flawlessly. Users could now effortlessly create personalized star maps, and the entire process, from payment to print-on-demand fulfillment, was automated.

Key Takeaways:

Building "Tu Cielo de Noche" was a challenging yet incredibly rewarding experience. It's a testament to the power of modern web technologies and the ability to turn complex scientific data into a personalized and meaningful experience.