Skip to main content

Overview

SkyTeam ROBLOX provides a comprehensive flight management system that tracks flights through their entire lifecycle. Airlines can create, start, monitor, and end flights through the API, with real-time updates to passengers and Discord integration.

Flight Lifecycle

Flights progress through several distinct stages:
1

Upcoming

Flight is created and scheduled with departure/arrival information
2

Started

Server marks the flight as started when the ROBLOX game begins
3

Active (Tick)

Game sends heartbeat signals to keep the flight active
4

Ended

Flight completes and miles are awarded to passengers

Flight Schema

Flights are stored in the database with the following structure:
packages/database/src/schema.ts
export const flights = pgTable("flights", {
  id: uuid("id").defaultRandom().primaryKey(),
  code: text("code").notNull(),
  gameId: text("gameId").notNull(),
  aircraft: text("aircraft").notNull(),
  airlineId: text("airlineId")
    .notNull()
    .references(() => airlines.airlineId),
  brandId: text("brandId")
    .notNull()
    .references(() => brands.brandId),
  startTime: timestamp("startTime").defaultNow().notNull(),
  endTime: timestamp("endTime"),
  codeshareAirlineId: text("codeshareAirlineId").references(
    () => airlines.airlineId,
  ),
  departure: text("departure").notNull(),
  arrival: text("arrival").notNull(),
  startedAt: timestamp("startedAt"),
  discordEventLink: text("discordEventLink")
    .notNull()
    .default("https://discord.gg/skyteam"),
});

API Endpoints

Fetch Upcoming Flights

Retrieve all flights that haven’t ended yet for an airline, including brand information:
apps/api/src/routes/flight.ts
router.get("/flight/fetchUpcomingFlights", async (_req, res, next) => {
  try {
    const airline = res.locals.airline as { airlineId: string };

    const [flights, brands] = await Promise.all([
      fetchComingFlights(airline.airlineId),
      fetchAirlineBrands(airline.airlineId),
    ]);

    const brandsById = new Map(brands.map((b) => [b.brandId, b]));
    const withBrand = flights.map((f) => ({
      ...f,
      brand: brandsById.get(f.brandId) || null,
    }));

    res.json(withBrand);
  } catch (err) {
    next(err);
  }
});

Note

This endpoint automatically enriches flight data with brand information for display purposes.

Start Flight

Mark a flight as started when the ROBLOX game server begins:
apps/api/src/routes/flight.ts
router.post("/flight/:id/serverStart", async (req, res, next) => {
  try {
    const { id } = req.params;
    const updated = await startFlight(id);
    if (!updated)
      return res.status(404).json({ error: "Flight not found" });
    res.json(updated);
  } catch (err) {
    next(err);
  }
});
The startFlight function sets the startedAt timestamp:
packages/database/src/flights.ts
export async function startFlight(id: string): Promise<Flight | null> {
  const result = await db
    .update(flights)
    .set({ startedAt: new Date() })
    .where(eq(flights.id, id))
    .returning();
  return result[0] || null;
}

Heartbeat (Tick)

Games send periodic heartbeat signals to indicate the flight is still active:
apps/api/src/routes/flight.ts
router.post("/flight/:id/tick", async (req, res, next) => {
  try {
    const { id } = req.params;
    // In future: persist lastPingAt and auto-end if stale
    res.json({ ok: true, flightId: id, message: "tick received" });
  } catch (err) {
    next(err);
  }
});

Future Enhancement

The schema will be expanded to include lastPingAt to automatically end flights that become stale.

End Flight

Mark a flight as completed:
apps/api/src/routes/flight.ts
router.post("/flight/:id/serverEnd", async (req, res, next) => {
  try {
    const { id } = req.params;
    const updated = await endFlight(id);
    if (!updated)
      return res.status(404).json({ error: "Flight not found" });
    res.json(updated);
  } catch (err) {
    next(err);
  }
});
The endFlight function sets the endTime timestamp:
packages/database/src/flights.ts
export async function endFlight(id: string): Promise<Flight | null> {
  const result = await db
    .update(flights)
    .set({ endTime: new Date() })
    .where(eq(flights.id, id))
    .returning();
  return result[0] || null;
}

Flight Passengers

Passengers are tracked separately with miles earned:
packages/database/src/schema.ts
export const flightPassengers = pgTable("flightPassengers", {
  flightId: uuid("flightId")
    .notNull()
    .references(() => flights.id),
  userId: text("userId")
    .notNull()
    .references(() => users.userId),
  miles: integer("miles").notNull(),
  joinedAt: timestamp("joinedAt").defaultNow().notNull(),
});

Fetching User Flights

Retrieve all flights a user has participated in:
packages/database/src/flights.ts
export async function fetchUserFlights(
  userId: string,
): Promise<(Flight & { miles: number })[]> {
  const result = await db
    .select({
      id: flights.id,
      code: flights.code,
      gameId: flights.gameId,
      aircraft: flights.aircraft,
      airlineId: flights.airlineId,
      brandId: flights.brandId,
      startTime: flights.startTime,
      endTime: flights.endTime,
      codeshareAirlineId: flights.codeshareAirlineId,
      departure: flights.departure,
      arrival: flights.arrival,
      startedAt: flights.startedAt,
      discordEventLink: flights.discordEventLink,
      miles: flightPassengers.miles,
    })
    .from(flights)
    .innerJoin(flightPassengers, eq(flights.id, flightPassengers.flightId))
    .where(eq(flightPassengers.userId, userId));

  return result;
}

Best Practices

Always Start Flights

Call /flight/:id/serverStart as soon as the ROBLOX game begins

Send Heartbeats

Send tick requests every 30-60 seconds during active flights

End Flights Promptly

Call /flight/:id/serverEnd immediately when the game completes

Include Discord Links

Provide Discord event links for community engagement

Next Steps

Learn About Miles

Discover how passengers earn and spend miles from flights

Build docs developers (and LLMs) love