- Trading-card fan in the page head. Up to five featured builders sit beside the title as cards. Each card shows the builder's level, photo, @username, profession and state, two skills, and either Verified or their experience level. The fan plays by itself and has a pause control. Tab brings a card to the front, and clicking a card opens that profile.
- New "Builders by state" section. There is one card for each state and federal territory, ranked by number of builders. Each card shows the state's outline, its rank and count, and a "See builders" link. The top 3 show first. "See all 16 states" brings in the rest, and "Show the top 3" folds them away again.
- A state card filters the directory. Pressing one narrows the list to that state, keeps any level and skill filters, and scrolls to the result count.
- Holds up when data fails. If the state counts or reference lists can't load, the section hides instead of showing dead links. "Try again" brings it back with the filters kept. With no featured builders, the head shows its text only.
- The eyebrow pill above the title is gone, so the page opens on its heading. All new text is in English and Bahasa Melayu.
AI Builder Hub update, 14 Oct 2026
What's new since the 13 Oct report: a new onboarding, notifications, Connect and Messages between builders, and a clearer profile.
What's new
2-minute read- Onboarding: one question at a time in a full window, welcomed by aRDi, ending on the builder's own card
- Notifications: a bell and a notifications page; joiners hear when an event changes, is cancelled, or starts tomorrow
- Connect and Messages: builders can ask to connect with a note, then message each other in an inbox
- Profile: details shown as cards tagged Public or Private, the project title as the heading, and a guide to Connect GitHub
Onboarding
/onboardingMergedNotifications
/notificationsMergedConnect and Messages
/builders/:username · /inboxMergedProfile
/profileAlso merged: English now loads on demand, like Bahasa Melayu, so the first download is about 100 kB smaller. Many Events fixes from the review of PR #30 are in too.
Screenshots from the app running locally on feat/profile-details-cards (f113118), 14 Oct 2026, using the local sign-in stub's test accounts. Click any image to enlarge it.
AI Builder Hub update, 11 to 13 Oct 2026
What shipped in the last three days across Builders, Profile, Projects, Create Project and Events, plus v1 of the API doc for MyDigital and DataFlows.
What's new
2-minute read- Builders: featured builders shown as a fan of trading cards, and a ranking of builders by state
- Projects: each project has a type and the problem it solves, with type filters, search, and featured projects first
- Events: a featured carousel, Hub and Builder Community sections, and filters
- RD API: the dashboard card endpoint is live on the sandbox, and API doc v1 is at the bottom
Builders
/buildersProjects
/projects · /projects/:slugCreate Project
/projects/newEvents
/eventsProfile
/profileScreenshots from the app running locally on main (bd4952d) with the fixture data, 13 Oct 2026. Click any image to enlarge it.
Progress update
11 to 13 OctEverything below is merged to main and running on the preview (sandbox). Dates are when the work merged.
Highlights
- Builders opens with a fan of trading cards showing featured builders, and a new Builders by state section ranks every state and federal territory.
- Every project now has a type and "the problem it solves". The Showcase filters by type, has a search box, and lists featured projects first.
- The Create Project form asks for the type with a row of icon tiles, and its placeholders are now writing prompts.
- The Events page has a featured carousel and separate Hub and Builder Community sections. You can search and filter by month and category.
- RD's dashboard card has its endpoint,
GET /api/sync/v1/builder-summary. It is live on the sandbox and documented below as API doc v1.
Changes by page
- Quieter upcoming-event cards. The help lines under Join ("Adds this event to your profile") and Joined ("Select Joined to remove it") no longer show on screen, though screen readers still read them. Joined now fills its half beside View at the same height.
- Skeleton cards while loading. Three shimmering placeholder cards show while upcoming events load, instead of a status line. They stay still when reduced motion is on.
- "Projects shipped" counts private projects on your own profile. Your own view counts every project you've published, public or private. Your public profile still counts public projects only. RD's dashboard card uses the same rule.
- The eyebrow pill above the title is gone.
- Project type and problem. Every project has one of 15 types. Cards show the type on one line and cut long names with an ellipsis. The project page has a new "The problem it solves" section, placed after About.
AgricultureEducationHealthFintechTransportEnvironmentSmall businessTourismCulture and heritageGovernmentAccessibilitySafety and scamsProductivityGamesOther
- Filter by type. The chips are now All types, Featured, then each type that has projects. They replace the old skill filter. The home page's projects band filters by type too.
- Featured first, drawn dark. Featured projects lead the list and use the trading cards' dark violet background, at the same size as the other cards.
- Search. A search box in the page head matches words in a project's title, summary or problem. It searches once you pause typing and stays applied when you change chips. If results take more than 150 ms, a spinner shows and the cards dim.
- Denser grid. Rows hold up to four cards, and each page shows 12 projects (was 10).
- Covers keep their shape. A cover shows at its own ratio over a blurred copy of itself, so it is never cropped or stretched. This applies on the card, the project page and the form preview.
- The eyebrow pill is gone from the list and project pages. A project page keeps that row only for the owner's status and Private pills.
- Two new required fields: Project type and "The problem it solves". The problem is plain text, up to 500 characters. Both are needed to create or edit a project. Imported projects and projects made before 12 Oct have neither until their owner edits them.
- Type picker as icon tiles. The types sit in a sideways row of tiles, each with its own line icon (13 new icons). The row's edges fade where more tiles are out of view.
- Prompts instead of example text. The placeholders now suggest what to write. The title help is shorter, and the description has a prompt.
- Stack picker polish. It is labelled "stack". Its filter box is full width and has a label for screen readers. Chips fade in and out as you type.
- The eyebrow pill above the form's title is gone.
- Featured carousel in the head. The title and lead sit over up to three featured events, shown one wide slide at a time. Slides change every 7 seconds, with pause, previous, next and dot controls. The carousel stays still when reduced motion is on.
- Two sections. Events are split into Hub events and Builder Community events (today these come from KrackedDevs). Each section is grouped by month in Malaysia time.
- Filters you can share. There is a search by event name, a month menu and category chips, and all three are saved in the URL. Choosing a category shows only its section and scrolls to it. When nothing matches, an empty state offers "Clear filters".
- Builder Community naming and logo. The "KrackedDevs" source label now reads "Builder Community". KD events show the KrackedDevs logo in a circle on the card. Links to KD's own site keep its name.
- Event page. "When" fits on one line and names the time zone once, for example "14 Oct 2026, 8:30 pm - 14 Oct 2026, 9:30 pm (MYT)".
- Skeleton cards while loading. A featured placeholder and six card placeholders show on /events, and three on the /welcome events step. If loading fails, the error notice shows with no skeleton.
Behind the scenes
- RD dashboard card endpoint (story E7-S03):
GET /api/sync/v1/builder-summary, protected by RD's server key. It has its own limit of 600 calls a minute and logs an audit row on every call. Live from revision00016. - Integration docs:
docs/integration/integration-guide.mdandopenapi.yaml, checked against the code. - Data integrity: the database now requires a project's type and problem to be both set or both empty.
- Preview demo data: 25 demo and fixture projects were filled with a type and problem in one transaction, so the new filters have content to show.
- Bahasa Melayu on every page, with a language switch in the header, went live on 11 Oct (revision
00011). The same deploy added help text for the onboarding options "Just exploring for now" and State.
To watch
- Older projects have no type. Projects created before 12 Oct, other than the demo data, stay untyped until their owners edit them, so type filters won't show them.
- KrackedDevs logo licence still needs to be confirmed (noted in
docs/data-sources.md). - The API guide already shared lists an endpoint that doesn't exist. It has
GET /api/projects/featured, which returns 404 on the sandbox. API doc v1 below points toGET /api/homeinstead. Please confirm before it goes to DataFlows. - The open questions for RD, such as whether both sites receive the same MyDigital ID
sub, are still open (API doc section 7).
Preview deploys
| Date | Revision | What went out |
|---|---|---|
| 11 Oct | rd-hub-00011-wr7 | Bahasa Melayu on every page; onboarding help text |
| 12 Oct | rd-hub-00012-4r4 | Builders trading-card fan and Builders by state (PR #2) |
| 12 Oct | rd-hub-00013-48v | Review fixes: fan shows featured builders only; state links keep filters (PR #19) |
| 12 Oct | rd-hub-00014-v2v | Review fixes: state cards when lists fail; shared card colour tokens (PR #29) |
| 12 Oct | rd-hub-00015-dd8 | Events carousel, sections, filters, KD logo; quieter profile event cards (PR #30) |
| 12 Oct | rd-hub-00016-6tw | Builder summary endpoint for RD's dashboard card (PR #43) |
| 13 Oct | rd-hub-00017-j2d | Project type and problem, type filter, search, featured first, form polish, event skeletons (PR #44) |
AI Builder Hub API for RD
v1 · 13 Oct 2026From RapidScreen (M) Sdn. Bhd., to MyDigital and DataFlows, for Rakyat Digital (RD). This follows the structure of the 12 Oct draft guide (v0.2) and replaces it. Every endpoint was checked against the code on main (bd4952d) and called on the sandbox on 13 Oct. No key appears on this page. RD's server key is sent separately through a one-time secret link.
What changed from the 12 Oct draft
| Area | Draft v0.2 | v1 (matches the code) |
|---|---|---|
| Featured projects | GET /api/projects/featured | GET /api/home, field data.featured_projects. The draft's path returns 404. |
| Project fields | No type | New project_type: one of 15 values, or null for older projects. |
| Card, linked answer | level and the two figures | Also returns username, is_listed and profile_url. |
start_url | <hub>/start | <hub>/login. The Hub has no /start page. Use the value as given. |
| More projects | None | Optional: GET /api/projects?featured=true, paginated, for more than five. |
Contents
- What the Hub gives RD
- How a learner is linked
- RD's server key
- The dashboard card
- RD's AI Builder page: public reads
- Limits, times and errors
- Open questions for RD
- Environments
- OpenAPI file
1. What the Hub gives RD
The AI Builder Hub (Hab Pembina AI, "the Hub") is a separate site with its own database. Learners sign in with MyDigital ID, the same way they sign in to rakyatdigital.gov.my. RD's AI Builder page is the front door, and a learner arriving from RD goes straight to the Hub. RD reads four things from the Hub, and each one fills one place on RD's site.
| RD needs | Shown on | Hub endpoint | Access |
|---|---|---|---|
| A learner's Level, Projects shipped and Events joined | "AI Builder" card on the learner dashboard | GET /api/sync/v1/builder-summary | RD's server key |
| Featured projects | RD's AI Builder page | GET /api/home | Public |
| Upcoming events, with featured ones marked | RD's AI Builder page | GET /api/events | Public |
| Hub totals: builders, projects, upcoming events | RD's AI Builder page | GET /api/stats | Public |
What the Hub does not send. It sends no copies of builder records, project records or event joins. It sends figures for one learner at a time, plus public content that anyone can already see on the Hub. A learner's name, email, MyKad number, private project details and XP points never leave the Hub.
No sync is needed. RD and the Hub both sign people in with MyDigital ID, so the MyDigital ID links an RD learner to a Hub builder (section 2). The card reads the Hub live, so RD never stores Hub data.
2. How a learner is linked
When a learner signs in, MyDigital ID gives each site an identifier for that person: the sub claim in the ID token (standard OpenID Connect). It is a random code, not a MyKad number, name or email. RD's MyDigital ID login already receives it.
- If RD and the Hub receive the same
subfor the same person, thatsubis the link, and nothing else is needed. RD sends it with each card request. - If each site receives its own
sub, the link needs one extra step. The first time a learner follows the card into the Hub, RD passes a one-time code and the Hub stores the pair. We will design this only if RD confirms thesubvalues differ.
MyDigital ID supports both (its published configuration lists subject_types_supported: public, pairwise) and sets one per registered site. This is open question 1.
3. RD's server key
The card endpoint needs a key that belongs to RD's server. It is not a learner's key: learners never see it, and RD's server sends the same key on every card request. It tells the Hub the request comes from RD.
- Send it only in the header:
Authorization: Bearer <key>. A key in a URL (astoken,access_tokenor any other parameter) is refused, even if the header is also correct, and the key counts as leaked. - The Hub team sends the key to your developer once, through a one-time secret link. Keep it in your server's secret store. Never put it in a browser, email, chat, ticket, log or code repository.
- There is one key per environment: sandbox now, staging and production later. When a key is replaced, the old one stops working at once, at a time we agree with you first.
- Every refusal (no key, wrong key, key in the URL) gets the same
401. After 10 refusals in a minute from one address, every call from that address gets429until the minute ends, even with the right key. Do not retry a 401 in a loop. - If you think the key has leaked, tell the Hub team and we will replace it at once.
- The public endpoints in section 5 need no key.
4. The dashboard card: GET /api/sync/v1/builder-summary
Call it from RD's server when a learner opens the dashboard.
Request. No query parameters. The learner's MyDigital ID sub goes in the X-Mydid-Sub header, never in the URL, because servers keep URLs in their logs.
| Header | Rule |
|---|---|
Authorization | Bearer <key>, as in section 3. |
X-Mydid-Sub | Required. 1 to 255 characters of valid UTF-8, with no control characters and not blank. Send exactly one. |
curl -sS \ -H "Authorization: Bearer $HUB_KEY" \ -H "X-Mydid-Sub: example-sub-0001" \ "$HUB/api/sync/v1/builder-summary"
Response. 200, in one of two shapes. A learner is linked when the Hub has a builder with that sub who finished onboarding (which includes consent) and is not deleted. Everyone else is not linked, including someone who signed in but hasn't finished onboarding.
{
"data": {
"linked": true,
"username": "aisyah-builds",
"is_listed": true,
"profile_url": "https://<hub>/builders/aisyah-builds",
"start_url": "https://<hub>/login",
"level": 2,
"projects_shipped": 3,
"events_joined": 4
}
}{
"data": {
"linked": false,
"start_url": "https://<hub>/login"
}
}| Field | Type | Meaning |
|---|---|---|
linked | boolean | true when the learner has finished onboarding on the Hub. |
username | string | The builder's Hub username. Linked answers only. |
is_listed | boolean | Whether others can see the profile right now: the builder chose public, and the profile isn't hidden or suspended. |
profile_url | string or null | The public profile page, <hub>/builders/<username>. null unless is_listed is true. |
level | integer | The Hub level, 1 or higher. |
projects_shipped | integer | The learner's published projects, public and private, not deleted. |
events_joined | integer | Events the learner has joined and not left: Builder Community events, and Hub events that haven't been deleted. |
start_url | string | Where the card's arrow goes. It signs the learner in to the Hub with MyDigital ID. A newcomer completes onboarding, including consent, first. Use it exactly as given, because the path may change. |
The figures belong to the learner whatever their profile's visibility. A private, hidden or suspended builder still gets their figures, because RD shows the card to that learner only. The answer never includes the sub, a name, an email, XP points, or the reason for a hide or suspension.
What the card shows. If linked: Level, Projects shipped, Events joined, and an arrow to start_url. If not linked: "Get started", or zeros if RD's design prefers (open question 5), with the arrow to start_url.
Rules on RD's side. The Hub can't see where a sub came from, so these rules stop one learner from reading another's card:
- Take the
subonly from the learner's own sign-in session on RD's server. Never take it from anything the browser sends, such as a link, form, cookie or header. - Remove any
X-Mydid-Subheader that arrives from a browser before calling the Hub. - Cache each answer in RD's application for a few minutes (for example, 5), keyed by that learner's
sub, and show it only to that learner. The Hub marks answersno-store, so HTTP caches won't keep them. - If the call fails (401, 429, 5xx or a timeout), show the card without figures and keep the arrow. Don't let it delay the dashboard.
Errors. 401: a key problem (section 3). 422: fields.mydid_sub fails required, string, max:255 or regex, or the URL has a query parameter (reported as prohibited under that parameter's name). 429: too many calls (section 6). 500: retry later.
5. RD's AI Builder page: public reads
These need no key and return only what the Hub already shows everyone. The Hub sends no CORS headers, so a page on another site can't call them from the browser. Call them from RD's server and cache each answer for 1 to 5 minutes. Each calling address may make 120 calls a minute.
5.1 Featured projects: GET /api/home changed
This returns the Hub's home content. RD needs data.featured_projects: up to 5 public projects that an admin featured and that have a cover, newest featured first. The same answer also includes projects (up to 8 public projects, featured first) and project_types (up to 5 type values that the most projects use). RD can ignore both. The Hub refreshes which projects are chosen every 60 seconds, and it re-checks every project on each call, so a project hidden or made private disappears from the next answer.
{
"data": {
"featured_projects": [
{
"slug": "tamu-stall-finder-fx46",
"title": "Tamu stall finder",
"summary": "Shows which stalls sell what at the weekly tamu, posted by the sellers.",
"project_type": "small_business",
"cover_url": "https://<hub>/storage/media/covers/bdd1923d-….webp",
"skills": [{ "slug": "react-native", "name": "React Native" }],
"source": "manual",
"is_featured": true,
"builder": { "username": "aidiljumadil", "avatar_url": null, "is_verified": false },
"published_at": "2026-10-01T03:30:00Z"
}
],
"projects": [ … ],
"project_types": ["agriculture", "tourism", "transport", "environment", "small_business"]
}
}Link each project to https://<hub>/projects/<slug>. summary, avatar_url and project_type may be null. source is git (imported from GitHub) or manual.
project_type value | Label (EN) | Value | Label (EN) |
|---|---|---|---|
agriculture | Agriculture | tourism | Tourism |
education | Education | culture | Culture and heritage |
health | Health | civic | Government |
fintech | Fintech | accessibility | Accessibility |
transport | Transport | safety | Safety and scams |
environment | Environment | productivity | Productivity |
small_business | Small business | games | Games |
other | Other |
GET /api/projects?featured=true&per_page=12 returns every featured public project, featured first, as {data, meta: {page, per_page, total, last_page}}. Unlike /api/home, it includes projects without a cover. per_page defaults to 20, and anything above 50 is served as 50. It also accepts project_type and q (search, up to 80 characters).5.2 Upcoming events: GET /api/events
Events that haven't ended, soonest first. This covers the Hub's own events and community events from the KrackedDevs feed, which the Hub labels "Builder Community". Show the ones with is_featured: true, or show all of them.
| Parameter | Rule |
|---|---|
limit | 1 to 50. Default 50. |
month | Optional. YYYY-MM, a month in Malaysia time, up to six months ahead. Returns only events starting that month. |
{
"data": [
{
"source": "hub",
"slug": "building-with-open-data-webinar-fx62",
"title": "Building with open data webinar",
"description": "Where to find Malaysian open data, how to read it, and three small projects …",
"event_type": "webinar",
"start_at": "2026-10-14T12:30:00Z",
"end_at": "2026-10-14T13:30:00Z",
"is_online": true,
"location": null,
"state": null,
"organizer_name": "Online builders circle",
"tags": [],
"banner_url": null,
"kd_url": null,
"external_url": null,
"is_featured": false,
"is_started": false,
"is_past": false
}
],
"meta": { "fetched_at": "2026-10-13T03:38:22Z", "stale": false, "kd_available": true }
}Fields RD is likely to show: title, start_at, end_at (may be null), is_online, location, state ({code, name} or null), organizer_name, event_type, banner_url (may be null) and is_featured. source is hub or kd. Link each event to https://<hub>/events/<source>/<slug>. In meta, stale: true or kd_available: false means the community feed is behind or unavailable, and the Hub's own events are still complete.
5.3 Hub totals: GET /api/stats
{
"data": {
"builders_count": 25,
"projects_count": 19,
"upcoming_events_count": 18,
"computed_at": "2026-10-13T03:38:22Z"
}
}The totals count publicly visible builders, public projects, and upcoming events (Hub and Builder Community). The Hub recalculates them every 60 seconds. The sample shows sandbox demo figures.
6. Limits, times and errors
| Endpoint | Limit | Counted by |
|---|---|---|
| Builder summary | 600 calls a minute | Key. Tell us if RD's traffic needs more even with caching. It's a setting. |
| Refused key calls | 10 a minute | Calling address (IPv4, or IPv6 /64). After 10 refusals, every call from that address gets 429 until the minute ends. |
| Public reads | 120 calls a minute | Calling address |
- Successful responses carry
X-RateLimit-LimitandX-RateLimit-Remaining. When you go over a limit, the response is429 too_many_requestswith aRetry-Afterheader in seconds. Wait that long before retrying. - Times are ISO 8601 in UTC with
Z, in whole seconds. Show them in Malaysia time (MYT, UTC+8). - Empty values are
null, never missing keys. Empty lists are[]. The only answer with fewer keys is the card's not-linked answer. - Only GET is allowed. Any other method gets
405 method_not_allowed. - Ignore fields you don't use. We may add fields, but we won't remove one without telling you.
Every error has the same shape. fields appears only on 422, and no error ever echoes back a value you sent:
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"request_id": "0199d6d5-6e7f-7081-9293-a4b5c6d7e8f9",
"fields": { "mydid_sub": ["required"] }
}
}| Status | code | What to do |
|---|---|---|
| 401 | unauthenticated | Check the key and the header. Don't retry in a loop. |
| 404 | not_found | Check the URL. |
| 405 | method_not_allowed | Use GET. |
| 422 | validation_failed | Fix the request. fields names each failed rule. |
| 429 | too_many_requests | Wait Retry-After seconds. |
| 500 | server_error | Retry later. If it keeps happening, report it with the request_id. |
| 503 | service_unavailable | Retry later. |
Every response, successful or not, has an X-Request-Id header with the same value as request_id. Quote it when you report a problem.
7. Open questions for RD
- Same
sub? Does MyDigital ID give RD and the Hub the samesubfor the same person? (Section 2.) - Silent sign-in. When a learner already signed in on RD follows the card's arrow, can MyDigital ID sign them in to the Hub without asking again? If not, they see the MyDigital ID sign-in once.
- Domains and client registration. We need the Hub's staging and production addresses, and its MyDigital ID client registration for each.
- "Sync". Syncing the two databases came up in the first meeting. What did RD need it for? Section 4 covers the card. If RD needs counts for reporting (for example, progress toward 1 million builders by 2030), we can add an endpoint with totals by state and month and no personal data.
- The not-linked card. Should it show "Get started", or zeros with the arrow?
- After onboarding. Should a new builder stay on the Hub after onboarding, or go back to RD's dashboard?
- Featured projects endpoint. Is
GET /api/homefine for DataFlows, or should the Hub addGET /api/projects/featuredas named in the draft?
8. Environments
| Environment | Base URL | Data |
|---|---|---|
| Sandbox | https://rd-hub-248055939850.asia-southeast1.run.app | Demo data only: made-up builders, projects and joins. Don't send a real learner's sub. Ask us for demo values to test a linked card. |
| Staging | To be confirmed (question 3) | Later |
| Production | To be confirmed (question 3) | Real builders, later |
9. OpenAPI file
The same four endpoints, plus the optional project list, as an OpenAPI 3.1 contract you can load into Swagger UI, Redoc, Postman or a client generator.
Show openapi.yaml (v1.0.0)
openapi: 3.1.0
info:
title: AI Builder Hub API for Rakyat Digital
version: 1.0.0
description: >-
What RD reads from the AI Builder Hub: one learner's figures for the dashboard card (RD's server key), and public
featured projects, upcoming events and totals for RD's AI Builder page (no key). No record sync. See the
integration guide for the rules on RD's side.
servers:
- url: https://rd-hub-248055939850.asia-southeast1.run.app
description: Sandbox (the Hub's preview, demo data only)
- url: https://{production_host}
description: Production, to be confirmed
variables:
production_host:
default: to-be-confirmed.example
components:
securitySchemes:
rdServerKey:
type: http
scheme: bearer
description: RD's server key, one per environment, sent only in the Authorization header. Never a learner's key.
headers:
RequestId:
description: The request's id; quote it when reporting a problem.
schema: { type: string }
schemas:
Error:
type: object
required: [error]
properties:
error:
type: object
required: [code, message, request_id]
properties:
code:
type: string
examples: [unauthenticated, not_found, method_not_allowed, validation_failed, too_many_requests, server_error, service_unavailable]
message: { type: string }
request_id: { type: string }
fields:
type: object
description: On 422 only. Each failed field or parameter, with the names of the rules it failed.
additionalProperties: { type: array, items: { type: string } }
BuilderSummaryLinked:
type: object
required: [linked, username, is_listed, profile_url, start_url, level, projects_shipped, events_joined]
properties:
linked: { const: true }
username: { type: string }
is_listed: { type: boolean, description: Public by the builder's choice, not hidden, not suspended. }
profile_url: { type: [string, 'null'], format: uri, description: Null unless is_listed. }
start_url: { type: string, format: uri, description: "The card's arrow. Use as given." }
level: { type: integer, minimum: 1 }
projects_shipped: { type: integer, minimum: 0, description: Published projects, public and private. }
events_joined: { type: integer, minimum: 0, description: Events joined and not left. }
BuilderSummaryNotLinked:
type: object
required: [linked, start_url]
properties:
linked: { const: false }
start_url: { type: string, format: uri }
NamedRef:
type: object
required: [slug, name]
properties:
slug: { type: string }
name: { type: string }
State:
type: object
required: [code, name]
properties:
code: { type: string, pattern: '^MY-(0[1-9]|1[0-6])$' }
name: { type: string }
ProjectType:
type: [string, 'null']
enum: [agriculture, education, health, fintech, transport, environment, small_business, tourism, culture, civic,
accessibility, safety, productivity, games, other, null]
description: Null on a project imported or made before types existed, until its owner edits it.
ProjectCard:
type: object
required: [slug, title, summary, project_type, cover_url, skills, source, is_featured, builder, published_at]
properties:
slug: { type: string, description: 'Link to https://<hub>/projects/<slug>.' }
title: { type: string, maxLength: 80 }
summary: { type: [string, 'null'], maxLength: 160 }
project_type: { $ref: '#/components/schemas/ProjectType' }
cover_url: { type: [string, 'null'], format: uri }
skills: { type: array, items: { $ref: '#/components/schemas/NamedRef' } }
source: { type: string, enum: [git, manual] }
is_featured: { type: boolean }
builder:
type: object
required: [username, avatar_url, is_verified]
properties:
username: { type: string }
avatar_url: { type: [string, 'null'], format: uri }
is_verified: { type: boolean }
published_at: { type: string, format: date-time }
Event:
type: object
required: [source, slug, title, description, event_type, start_at, end_at, is_online, location, state,
organizer_name, tags, banner_url, kd_url, external_url, is_featured, is_started, is_past]
properties:
source: { type: string, enum: [hub, kd], description: 'hub: the Hub''s own event. kd: a Builder Community event from the KrackedDevs feed.' }
slug: { type: string, description: 'Link to https://<hub>/events/<source>/<slug>.' }
title: { type: string }
description: { type: [string, 'null'] }
event_type: { type: [string, 'null'], description: 'Hub events: workshop, hackathon, meetup, conference, webinar or other. Community events: the feed''s own text.' }
start_at: { type: string, format: date-time }
end_at: { type: [string, 'null'], format: date-time }
is_online: { type: boolean }
location: { type: [string, 'null'] }
state: { oneOf: [{ $ref: '#/components/schemas/State' }, { type: 'null' }] }
organizer_name: { type: [string, 'null'] }
tags: { type: array, items: { type: string } }
banner_url: { type: [string, 'null'], format: uri }
kd_url: { type: [string, 'null'], format: uri }
external_url: { type: [string, 'null'], format: uri }
is_featured: { type: boolean }
is_started: { type: boolean }
is_past: { type: boolean }
responses:
Unauthenticated:
description: No key, a wrong key, or a key in the URL. Every refusal gets this same answer.
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
TooManyRequests:
description: Over a limit, or 10 refused calls in a minute from this address. Wait the Retry-After seconds.
headers:
Retry-After: { schema: { type: integer } }
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
ValidationFailed:
description: A header or parameter failed a rule; see error.fields. No value you sent is echoed.
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
paths:
/api/sync/v1/builder-summary:
get:
operationId: getBuilderSummary
summary: One learner's figures for the dashboard card
description: >-
Call from RD's server only. Take the sub from the learner's own server-side session, never from the browser;
cache each answer per learner for a few minutes. Any query parameter is refused.
security: [{ rdServerKey: [] }]
parameters:
- name: X-Mydid-Sub
in: header
required: true
description: "The learner's MyDigital ID `sub` from RD's own sign-in session. Never in the URL."
schema: { type: string, minLength: 1, maxLength: 255 }
responses:
'200':
description: Linked or not linked. Cache-Control no-store.
headers:
X-Request-Id: { $ref: '#/components/headers/RequestId' }
content:
application/json:
schema:
type: object
required: [data]
properties:
data:
oneOf:
- $ref: '#/components/schemas/BuilderSummaryLinked'
- $ref: '#/components/schemas/BuilderSummaryNotLinked'
examples:
linked:
value:
data:
linked: true
username: aisyah-builds
is_listed: true
profile_url: https://rd-hub-248055939850.asia-southeast1.run.app/builders/aisyah-builds
start_url: https://rd-hub-248055939850.asia-southeast1.run.app/login
level: 2
projects_shipped: 3
events_joined: 4
notLinked:
value: { data: { linked: false, start_url: 'https://rd-hub-248055939850.asia-southeast1.run.app/login' } }
'401': { $ref: '#/components/responses/Unauthenticated' }
'422': { $ref: '#/components/responses/ValidationFailed' }
'429': { $ref: '#/components/responses/TooManyRequests' }
/api/home:
get:
operationId: getHome
summary: Featured projects (and the Hub home's project band)
description: >-
Public. featured_projects holds up to 5 featured public projects with a cover, newest featured first.
projects (up to 8) and project_types (up to 5) are the Hub home's band; RD may ignore them. Cache 1 to 5 minutes.
security: []
responses:
'200':
description: The home content.
content:
application/json:
schema:
type: object
required: [data]
properties:
data:
type: object
required: [featured_projects, projects, project_types]
properties:
featured_projects: { type: array, maxItems: 5, items: { $ref: '#/components/schemas/ProjectCard' } }
projects: { type: array, maxItems: 8, items: { $ref: '#/components/schemas/ProjectCard' } }
project_types: { type: array, maxItems: 5, items: { type: string } }
'429': { $ref: '#/components/responses/TooManyRequests' }
/api/projects:
get:
operationId: listProjects
summary: Public projects, paginated (optional for RD)
description: Public. Featured first (newest featured), then newest published. Use featured=true for featured only.
security: []
parameters:
- { name: featured, in: query, schema: { type: string, enum: ['true', 'false'] } }
- { name: project_type, in: query, schema: { type: string } }
- { name: q, in: query, description: Search title, summary and problem., schema: { type: string, maxLength: 80 } }
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
- { name: per_page, in: query, description: Above 50 is served as 50., schema: { type: integer, minimum: 1, default: 20 } }
responses:
'200':
description: One page of projects.
content:
application/json:
schema:
type: object
required: [data, meta]
properties:
data: { type: array, items: { $ref: '#/components/schemas/ProjectCard' } }
meta:
type: object
required: [page, per_page, total, last_page, types_in_use, featured_in_use]
properties:
page: { type: integer }
per_page: { type: integer }
total: { type: integer }
last_page: { type: integer }
types_in_use: { type: array, items: { type: string } }
featured_in_use: { type: boolean }
'422': { $ref: '#/components/responses/ValidationFailed' }
'429': { $ref: '#/components/responses/TooManyRequests' }
/api/events:
get:
operationId: listEvents
summary: Upcoming events, featured ones marked
description: Public. Events not yet ended, soonest first. Cache 1 to 5 minutes. No CORS; call from RD's server.
security: []
parameters:
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 50, default: 50 }
- name: month
in: query
description: YYYY-MM in Malaysia time, up to six months ahead.
schema: { type: string, pattern: '^\d{4}-\d{2}$' }
responses:
'200':
description: The events.
content:
application/json:
schema:
type: object
required: [data, meta]
properties:
data: { type: array, items: { $ref: '#/components/schemas/Event' } }
meta:
type: object
required: [fetched_at, stale, kd_available]
properties:
fetched_at: { type: [string, 'null'], format: date-time }
stale: { type: boolean }
kd_available: { type: boolean }
'422': { $ref: '#/components/responses/ValidationFailed' }
'429': { $ref: '#/components/responses/TooManyRequests' }
/api/stats:
get:
operationId: getStats
summary: Hub totals
description: Public. Publicly visible builders, public projects and upcoming events. Recounted every 60 seconds.
security: []
responses:
'200':
description: The totals.
content:
application/json:
schema:
type: object
required: [data]
properties:
data:
type: object
required: [builders_count, projects_count, upcoming_events_count, computed_at]
properties:
builders_count: { type: integer }
projects_count: { type: integer }
upcoming_events_count: { type: integer }
computed_at: { type: string, format: date-time }
'429': { $ref: '#/components/responses/TooManyRequests' }