Skip to content

Repository files navigation

Horizon Star Wars App

A Horizon federated app (Webpack Module Federation remote, built with @netsapiens/horizon-sdk) that adds a Star Wars page to the Horizon portal's Apps menu, alongside Example CRM and the Pokemon app.

Same shape as that app — own repo, own remoteEntry.js, published to GitHub Pages by CI — with a different public data source and the same single write: adopting an image as the signed-in user's avatar.

  • Search ~87 characters and droids by name, species, homeworld or allegiance. "jedi", "tatooine" and "rodian" all work, because the index is searched across every field rather than just the name.
  • A responsive card grid. Clicking a card opens a detail panel above it: portrait, species and gender, homeworld, born/died in BBY/ABY, height and mass, masters and apprentices, affiliations, and a link to Wookieepedia. Droids get their hardware — model, manufacturer — where a person gets a biography.
  • Set as my avatar crops the portrait to a square and uploads it as the signed-in user's avatar, through the host's audited API proxy.
  • Every visible element comes from horizonContext.ui, so the page re-themes live with the portal's dark/light toggle.

Architecture

Browser (https://<portal>/apps/starwars)
  └─ Horizon host loads the remote from the CDN
       https://netsapiens.github.io/horizon-starwars-app/remoteEntry.js
       └─ one fetch of the character index (static JSON, ~57 KB)
            https://raw.githubusercontent.com/akabab/starwars-api/…/all.json
       └─ portraits displayed straight from Fandom's image CDN
       └─ POST …/avatar through horizonContext.api   (the only write)

The index is one document holding complete records, so there is no per-character request and no detail-loading state. Reads need nothing from the platform; the avatar button is the only thing that does.

Why this data source

akabab/starwars-api is static JSON on raw.githubusercontent.com, which sends Access-Control-Allow-Origin: *, and its records are rich — species, homeworld, affiliations, master/apprentice lineage, birth and death.

SWAPI (and swapi.dev, swapi.tech) carries no images at all, and this app is about the picture. The Star Wars Databank API has 964 characters but only name/description/image, on a free instance that cold-starts.

Note what does NOT exist: a Star Wars equivalent of PokeAPI/sprites. Pokemon sprites are game assets a community vendored into a repository; Star Wars portraits are photographic stills owned by Lucasfilm, so every open dataset hotlinks someone else's CDN. That is the origin of the one caveat below.

Setting an avatar

Set as my avatar uploads to POST /domains/{domain}/users/{extension}/avatar through horizonContext.api, so the app never sees the user's token and the call is attributed to it on the Registered Apps page. src/api/avatarApi.ts has the contract. Four things govern whether it works:

  • The image is cropped to a square first. The endpoint rejects anything that is not 1:1 with a 400 rather than letterboxing it, and these portraits are tall. The crop is horizontally centred but biased toward the top, because a strictly centred square of a tall portrait frames a torso rather than a face.
  • The portrait's bytes must be readable cross-origin. The app fetches the image to a Blob and crops from that — a Blob is same-origin, so the canvas is never tainted and toBlob cannot throw. That leaves one requirement: the image CDN must send CORS headers on the fetch. If a portal ever fails here, INSTALL.md has the same-origin proxy that removes the question. Displaying a portrait never needs any of this, so the page works either way.
  • The platform's API-write master capability must be enabled for SDK apps (Platform -> UI SDK Management). With it off, the host's proxy answers 403 before the request leaves the browser, and the panel says so.
  • The session needs an extension. HorizonUser.extension is optional; a platform admin who is not a subscriber has no avatar to set, and the button is disabled with a note rather than failing at the API.

The upload is multipart/form-data with the field named file, because the endpoint reads a PHP file upload. The host's api client passes a FormData body through to fetch untouched and sets no Content-Type, letting the browser write the multipart boundary.

The host's top bar reads the avatar once per page load, so a new picture appears on the next load rather than instantly.

Hosting

Served from GitHub Pages, like horizon-sdk-demo and the Pokemon app:

https://netsapiens.github.io/horizon-starwars-app/remoteEntry.js

Opening that URL in a browser shows a blank page — this is a headless remote. It renders nothing on its own; it only mounts inside the Horizon host.

.github/workflows/deploy-pages.yml runs on every push to main: npm ci -> version guard -> npm run typecheck -> npm run build -> npm run verify -> .nojekyll -> upload -> actions/deploy-pages. The repo needs Settings -> Pages -> Source: GitHub Actions.

The version guard fails the build when src/, webpack.config.js, package.json or the lockfile changed while version did not. The remote entry URL is stable, so version is the only thing that tells the platform to re-verify: new bytes under an unchanged version leave it enforcing the OLD hash at the same URL, every host fails its integrity check, and the app silently stops appearing with no verdict to explain it.

Registration

field value
id horizon-starwars (derived server-side, not sent)
webpack_module horizonStarwars (must equal the MF name in webpack.config.js)
remote_entry_url https://netsapiens.github.io/horizon-starwars-app/remoteEntry.js
reseller *
enabled yes

Registering is not enough on its own: a newly registered app sits at verification status none and renders nowhere until the bundle is deployed and verified. INSTALL.md sections 2-3 have both calls.

Releasing an update

# 1. change code, and bump `version` in package.json — CI fails without it
# 2. commit and push to main
# 3. gh run watch          # Pages must finish BEFORE the next step
# 4. Registered Apps -> Deploy on the horizon-starwars row
# 5. read the verdict: approved | flagged -> loads; rejected -> fix and repeat

Develop

npm install
npm run dev        # remoteEntry.js on http://localhost:5009/remoteEntry.js
npm run typecheck
npm run build
npm run verify     # the same bundle checks the platform runs

Route

field value
route id starwars-characters
parentPath /apps (the Apps menu — Example CRM's menu)
path starwars -> /apps/starwars
placement { last: true }

No requiredScopes, matching Example CRM: /apps carries no section floor, the page reads only public data, and its one write acts on the caller's own avatar. A page that reads customer data must declare a tier instead.

Credits

Character data from akabab/starwars-api (MIT), sourced from Wookieepedia; portraits are served from Fandom's image CDN and are not redistributed by this repository. Star Wars and its characters are trademarks of Lucasfilm Ltd.; this is an unofficial SDK demo, not affiliated with or endorsed by Lucasfilm or Disney, and the imagery is theirs. Treat it as a demo of the Horizon SDK rather than something to ship to customers.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages