This guide is for developers who want to extend, customize, or modify Development Process Navigator.
This guide explains how to change Development Process Navigator. The codebase has three parts:
- Frontend: a Next.js 14 static app in
frontend/. - Backend: one Python Lambda in
backend/, organized as a modular monolith. - Infrastructure: a single CDK stack in
infrastructure/.
Most changes stay within one of these. This guide points you to the exact files for the changes people ask for most.
Read the Architecture Deep Dive first for the overall design.
- Project Structure Overview
- Frontend Modifications
- Backend Modifications
- Adding New Features
- Changing AI/ML Models
- Database Modifications
- Best Practices
- Testing Your Changes
├── frontend/
│ ├── app/ # Next.js App Router pages
│ │ ├── page.tsx # Login / Register / Forgot password
│ │ ├── globals.css # Theme, sidebar, layout classes
│ │ ├── instructor/ # Dashboard, exercises, roster, results
│ │ ├── student/ # Dashboard, exercise, history (+ detail), scores
│ │ └── tutorial/ # Help Center
│ ├── src/
│ │ ├── shared/ # apiClient, session, types, SidebarBase, InfoIcon, useRoleGuard
│ │ ├── student/RoundsBoard.tsx # Multi-round card-sort board + results screen
│ │ └── instructor/ # ScenarioCustomizer, SubmissionsDashboard, WeightMatrix, ...
│ └── next.config.js # output: "export"
├── backend/
│ ├── src/
│ │ ├── api/ # lambda_handler.py (entry), app.py (dispatch + handlers), router.py
│ │ ├── authoring/ # taxonomy.py (cards + weights), seed_templates.py (scenarios A–E)
│ │ ├── exercise/ # rounds.py (multi-round), scoring/scoring.py (pure core)
│ │ ├── identity/ results/ live_session/ shared/
│ └── tests/ # pytest suite
├── infrastructure/
│ ├── bin/processcanvas.ts # CDK app entry point
│ └── lib/processcanvas-stack.ts # All AWS resources
├── deploy.sh # Full deploy (CDK + frontend)
└── docs/
Location: frontend/app/globals.css
The CSS variables at the top of the file control the brand colors and layout sizes:
:root {
--asu-maroon: #8C1D40;
--asu-gold: #FFC627;
--sidebar-width: 220px;
--sidebar-collapsed: 48px;
--header-height: 56px;
}Many components also use inline style objects (a styles record at the bottom of each .tsx file) with hard-coded colors. In particular, RoundsBoard.tsx has CARD_TYPE_COLOR, which sets the color for each round. Search for the hex value when you re-theme.
Location: frontend/app/
- Create a folder for the route, for example
frontend/app/instructor/reports/. - Add a
page.tsxthat starts with"use client";. The app is a static export, so there's no server rendering. - Protect the page with
const allowed = useRoleGuard("INSTRUCTOR");and render nothing untilallowedis true. - Render the sidebar and wrap the content in
<main className="main-content">. Keep the sidebar as a sibling just beforemain, because the CSS uses that order to shift the content when the sidebar collapses. - Add a sidebar link in
frontend/src/shared/InstructorSidebar.tsxorSidebar.tsx(each is anavItemsarray of{ id, label, icon, href }). - Call the backend through
apifromfrontend/src/shared/session.ts. Add a method toApiClientinapiClient.tsfor any new endpoint.
Location: frontend/src/
- Round board (
src/student/RoundsBoard.tsx):RoundStagerenders the pool and the target columns.SequenceStagerenders Round 1.ResultsScreenandBudgetScheduleStepare the last two screens.
- Reusable cards:
REUSABLE_CARD_TYPES(currentlyPROFESSIONAL) decides which rounds keep cards in the pool after placement. To let tasks be reused too, add"TASK_DELIVERABLE". - Column help text: comes from
exercise.stages[].descriptionandexercise.activities[].description, rendered with<InfoIcon tooltip title color />. - Sidebar: both roles use
src/shared/SidebarBase.tsx. The collapsed state is saved inlocalStorage(pc_sidebarCollapsed).
You usually don't need a new Lambda. Add a route to the existing ApiFn instead (see the next section). Add a separate function only for work with different needs, such as a long-running job, a different memory size or timeout, or a different trigger.
Location: backend/src/<module>/ (handler) and infrastructure/lib/processcanvas-stack.ts (wiring)
- Write the handler in Python, inside the existing package so shared imports work:
# backend/src/results/export_handler.py import json def handler(event, context): # Your logic here return {"statusCode": 200, "body": json.dumps({"message": "Success"})}
- Add it to the stack next to
ApiFn:const exportFn = new lambda.Function(this, "ExportFn", { runtime: lambda.Runtime.PYTHON_3_12, handler: "src.results.export_handler.handler", code: lambda.Code.fromAsset("../backend"), timeout: Duration.minutes(2), environment: { ...Object.fromEntries(allTables.map((t) => [`TABLE_${t.node.id.toUpperCase()}`, t.tableName])) }, }); attempts.grantReadData(exportFn);
Location: infrastructure/lib/processcanvas-stack.ts
- Lambda settings (memory and timeout):
ApiFnhas a 29 s timeout, which matches API Gateway's limit, and 128 MB of memory. RaisememorySizeif scoring large classes gets slow. - Environment variables: add them to
ApiFn.environment. Never hard-code secrets. Read them from CDK context or the deploy environment, the wayinstructorAccessCodeis handled. - Outputs: add a
CfnOutputfor any value the frontend needs, then pass it into the build indeploy.sh. - Run
npx cdk diff --context instructorAccessCode=<code>before you deploy.
- Write the handler in
backend/src/api/app.py(or in a domain module thatapp.pycalls). Start with an authorization check:def get_exercise_summary(principal: Principal, exercise_id: str) -> dict: _require_role(principal, Role.INSTRUCTOR) ex = _exercise_record(exercise_id) if ex.get("ownerInstructorId") != principal.user_id: raise ForbiddenError("Not the owner of this exercise.") return {"exerciseId": exercise_id, "status": ex.get("status")}
- Add a route in
dispatch(), before the finalraise NotFoundError:if method == "GET" and len(seg) == 3 and seg[0] == "exercises" and seg[2] == "summary": return 200, get_exercise_summary(principal, seg[1])
- API Gateway:
- Signed-in routes need no change, because
/{proxy+}already forwards them through the Cognito authorizer. - Public routes (no login) must be added as explicit resources in the stack, the way
auth/registerandauth/loginare.
- Signed-in routes need no change, because
- Raise errors from
shared/errors.py(ValidationError400,ForbiddenError403,NotFoundError404,ConflictError409).lambda_handlerturns them into{"error": ...}responses. - Add an
ApiClientmethod in the frontend, and document the endpoint in APIDoc.md.
The login page already has a Forgot Password? flow, and forgot_password() and confirm_reset() exist in app.py. The routes just aren't reachable through API Gateway.
Files to modify:
infrastructure/lib/processcanvas-stack.tsbackend/src/api/app.py(only if you change the flow)
Steps:
- Add the public resources next to the existing auth routes:
authResource.addResource("forgot-password").addMethod("POST", integration); authResource.addResource("confirm-reset").addMethod("POST", integration);
- No IAM change is needed. The handlers call Cognito
ForgotPasswordandConfirmForgotPassword, which are public client APIs that use the app client ID, not the Lambda's role. - Check that the reset email arrives. Users are created with
email_verified=true, so Cognito sends the code with its default email sender. That sender has a low daily limit, so set up SES on the User Pool before you rely on it for a whole class. - Deploy, then try the full reset flow from the login page.
Because cards can be reused in Round 3, a student who drops a professional into every activity still gets full credit. Credit is capped at the card's best weight, and wrong placements cost nothing.
Files to modify:
backend/src/exercise/scoring/scoring.py(_earned_for)backend/tests/test_scoring.pyandtest_rounds.py
Steps:
- Decide on the rule. Two options:
- Score each correct activity out of the sum of the card's weights.
- Subtract a fixed penalty for each zero-weight placement, with a floor of 0.
- Change
_earned_foronly for the round types that should use the new rule.score_targetsis shared by Rounds 2–5 and by the olderscore()wrapper. - Update the tests and the scoring description in the User Guide FAQ.
Files to modify:
backend/src/authoring/taxonomy.py: the stages, the 15 major activities, the professional, task, and decision cards, and their activity weights from the Instructor Manual.backend/src/authoring/seed_templates.py: theSCENARIOSdictionary (A–E). Each scenario has anactivity_scalethat drops or reweights activities.
Steps:
- Edit the taxonomy or the scenario.
- Run
pytestinbackend/. - Deploy the backend. Note:
_ensure_seed_template()only adds templates that are missing. Existingseed-scenario-*rows in theTemplatestable aren't updated, so delete those rows, or change the template ID, to pick up your changes. - Exercises that are already published keep their frozen version. Publish a new exercise to use the new content.
Live sessions were removed from the product in August 2026. The infrastructure is still deployed: the WebSocket API, WsFn, and the Sessions, SessionParticipants, and WsConnections tables. So are backend/src/live_session/service.py and the unused startSession / joinSession client methods.
To remove the leftovers:
- In
processcanvas-stack.ts, delete the WebSocket API and stage,WsFnand its log group, and the three session tables (and remove those tables fromallTables). Also delete theWsApiUrloutput and theNEXT_PUBLIC_WS_URLbuild variable. - Delete
startSessionandjoinSessionfromapiClient.ts. - Deploy. CloudFormation deletes the resources, including any data in those tables.
To restore the feature:
- Create
backend/src/live_session/ws_handler.pywithhandler(event, context)handling$connect,$disconnect, and$default. - Add
/sessionsroutes todispatch()that calllive_session/service.py. - Push progress events to the instructor's connection using the
WS_ENDPOINTenvironment variable, and add an instructor page that listens for them.
The project doesn't use any AI or ML model. Scoring is deterministic and rule-based. If you add AI features, such as generated feedback on a student's reflection:
Location: a new module, for example backend/src/results/feedback.py, plus IAM in the stack.
- Grant the permission in the stack:
apiFn.addToRolePolicy(new iam.PolicyStatement({ actions: ["bedrock:InvokeModel"], resources: ["<model ARN>"] })). - Call it with
boto3.client("bedrock-runtime").converse(modelId=..., messages=[...]). - Put the model ID in an environment variable rather than in code, so you can switch models without changing code.
- Keep scoring deterministic. Use the model only for explanation text, never to decide the score.
There are no LLM prompts. The reflection prompt students see is built in the frontend (ResultsScreen in RoundsBoard.tsx) from the weakestMatch the scorer returns. Edit the text there.
- Define the table in
infrastructure/lib/processcanvas-stack.tsusing the existing helper:const reports = table("Reports", "exerciseId", "createdAt");
- Add it to the
allTablesarray. That givesApiFnread/write access and an environment variable namedTABLE_REPORTS, built from the construct ID in uppercase. - Use it from Python with
_t("Reports"), which readsTABLE_REPORTS. - Deploy with CDK.
DynamoDB has no schema, so new attributes don't need a migration. Keep these in mind:
- Keys can't be changed. Changing a table's partition or sort key means creating a new table and copying the data.
- Add a GSI for new lookups with
table.addGlobalSecondaryIndex(...)instead of scanning. Several existing lookups still scan (for exampleget_attemptand roster lookups), and those are good candidates. - Some nested data is stored as JSON strings, such as
roundPlacements,roundResults, andsnapshot. Change their shape in a backward-compatible way, because old attempts and exercises are still read. - Removal policy: every table uses
RemovalPolicy.DESTROY. Change it toRETAINbefore you store data you can't afford to lose.
- Test before deploying: run
pytestandnpx tsc --noEmit, and usecdk synthorcdk diffto check infrastructure changes. - Keep secrets out of the repo: supply them at deploy time (CDK context or environment variables), the way the instructor access code is handled.
- Follow existing patterns: route through
dispatch(), check roles with_require_role, raise errors fromshared/errors.py, and keep scoring pure. - Keep weights on the server: never add
mappingsor weights to the student board response. - Update the docs: keep APIDoc.md and this guide in sync with your changes.
- Use version control: make small, focused commits.
# Backend unit tests (pure scoring, rounds, authoring, identity)
cd backend
pip install -r requirements.txt
python3 -m pytest -q
# Frontend type-check and production build
cd frontend
npx tsc --noEmit -p .
npm run build
# Frontend dev server against a deployed API
echo "NEXT_PUBLIC_API_URL=<RestApiUrl without trailing slash>" > .env.local
npm run dev
# Infrastructure
cd infrastructure
npx cdk synth --context instructorAccessCode=<code># Backend-only change: update the Lambda code in place (zip from inside backend/)
cd backend && zip -rq /tmp/lambda_update.zip src/ -x "**/__pycache__/*"
aws lambda update-function-code --function-name <ApiFn name> --zip-file fileb:///tmp/lambda_update.zip
# Infrastructure change: faster Lambda-only updates during development
cd infrastructure
npx cdk deploy --hotswap --context instructorAccessCode=<code>See the Deployment Guide for frontend-only redeploys.
The project is built to be extended. Most features need a new route in dispatch(), a method in ApiClient, and a page or component in the frontend. Content changes go in taxonomy.py and seed_templates.py. If you build something useful, please consider contributing it back.
For questions or support, open an issue on the GitHub repository or contact the ASU AI Cloud Innovation Center team.