Backend
FastAPI backend for Nexus by McGuire Technology.
Development
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
npm run backend:devThe API will be available at http://127.0.0.1:8000.
Architecture
The backend follows a hexagonal layout:
backend/app/main.pyis the FastAPI composition root.backend/app/api/contains the HTTP API composition router and focused FastAPI route modules.backend/app/application/contains use-case services such as authentication and tenancy rules.backend/app/domain/contains request and response schemas used by the API boundary.backend/app/infrastructure/contains infrastructure adapters, including SQL session helpers and row mappers.backend/app/core/contains runtime configuration and platform constants.
Route handlers should stay thin and delegate business rules to application modules. Domain-specific API modules should live under backend/app/api/ and be included by the HTTP composition router. Database access helpers and SQL row mapping should stay behind infrastructure so future persistence changes do not leak into the HTTP layer.
System, project, education, asset, census, configuration, and location endpoints are split into focused backend/app/api/ modules. backend/app/api/router.py is now a composition router that includes those domain-specific route adapters.
Project workflows live in backend/app/application/projects.py. Asset workflows such as manufacturers, asset types, asset models, assets, assignments, and circulations now live in backend/app/application/assets/. Issue workflows live in backend/app/application/service.py, with asset repairs implemented as a project issue subtype that keeps asset-specific fields in the asset area. HTTP routes pass simple boundary inputs such as the current user and selected tenant filter into those use cases instead of resolving tenant scope, enforcing asset rules, and writing SQL inline.
Asset API routes live under backend/app/api/assets/, including manufacturers, asset types, asset models, inventory, assignments, circulations, and repair subtype compatibility. Configuration item API routes live under backend/app/api/configurations/. Project, issue, and project-scoped /me API routes live under backend/app/api/projects/.
Person and identity workflows now live in backend/app/application/census.py with matching route adapters under backend/app/api/census/. Location workflows live in backend/app/application/locations.py with route adapters in backend/app/api/.
Education workflows now live in backend/app/application/schools/ with matching entity-focused route adapters under backend/app/api/education/.
Tenant workflows now live in backend/app/application/tenants.py with matching route adapters and tenant-scoped /me routes under backend/app/api/system/.
User, group, membership, auth, session, preference, and user-scoped /me workflows are System module concerns. Their application services live in backend/app/application/users.py and related auth/tenant application services with matching route adapters under backend/app/api/system/.
Auth concerns now live in backend/app/application/auth/ and tenancy helpers now live in backend/app/application/tenancy/. SQL row mappers are split by domain under backend/app/infrastructure/mappers/.
Docker
The backend can also run behind Traefik in the Compose stack:
docker compose up --build backend postgres traefikBy default, the API is routed at http://api.localhost.
The container receives DATABASE_URL from Compose and waits for PostgreSQL to become healthy before starting. The default local connection string is:
postgresql://nexus:nexus_dev_password@postgres:5432/nexusDatabase Migrations
The backend uses Alembic for database migrations. The backend Docker container runs migrations automatically before starting the API:
alembic upgrade headFor local development, set DATABASE_URL and run the same command before starting the backend.
You can also run migrations through npm:
npm run backend:migrateSystem
System is a Nexus module. The backend uses OAuth2 password bearer tokens. Users can sign in with either their username or email address plus a password. User IDs and token IDs are ULIDs, and passwords are hashed with Argon2id before they are stored. Users are global accounts; tenant access comes from group memberships, with current and default tenant selection stored in user_sessions and user_preferences.
Default API routes:
POST /me/signupcreates a user and returns a bearer token.POST /me/signinsigns in with OAuth2 form fieldsusernameandpassword.POST /me/signoutrevokes the current bearer token.GET /mereturns the authenticated user.GET /me/tenantsreturns tenants available to the authenticated user.GET /me/groupsreturns the authenticated user's current-tenant groups.GET /me/projectsreturns projects available in the authenticated user's current tenant context.GET /me/current-projectreturns the authenticated user's selected project.POST /me/current-projectchanges the authenticated user's selected project.GET /me/issuesreturns issues for the authenticated user's selected project.GET /me/sessionsreturns the authenticated user's active sessions.DELETE /me/sessions/{session_id}deletes and revokes one of the authenticated user's sessions.GET /me/current-tenantreturns the authenticated user's selected tenant.POST /me/current-tenantchanges the authenticated user's selected tenant.GET /me/preferencesreturns the authenticated user's preferences.PATCH /me/preferencesupdates the authenticated user's preferences.GET /tenantsreturns tenant objects for authenticated requests.POST /tenantscreates a tenant.GET /tenants/{tenant_id}returns a tenant.PATCH /tenants/{tenant_id}updates a tenant.DELETE /tenants/{tenant_id}deletes a tenant.POST /tenants/{tenant_id}/archivearchives a tenant.POST /tenants/{tenant_id}/restorerestores an archived tenant.GET /tenants/{tenant_id}/groupsreturns tenant groups.GET /usersreturns user objects for authenticated requests.POST /userscreates a global user and adds them to the selected tenant's users group.GET /users/{user_id}returns a user.PATCH /users/{user_id}updates a user.DELETE /users/{user_id}deletes a user.POST /users/{user_id}/deactivatedeactivates a user.POST /users/{user_id}/restorerestores a deactivated user.GET /users/{user_id}/groupsreturns the groups a user belongs to.GET /groupsreturns group objects for authenticated requests.POST /groupscreates a group.GET /groups/{group_id}returns a group.PATCH /groups/{group_id}updates a group.DELETE /groups/{group_id}deletes a group.POST /groups/{group_id}/archivearchives a group.POST /groups/{group_id}/restorerestores an archived group.GET /groups/{group_id}/membershipsreturns user and nested group membership objects for a group.POST /groups/{group_id}/membershipsadds a user or nested group membership to a group.DELETE /groups/{group_id}/memberships/{membership_id}removes a membership from a group.
The initial Alembic migration creates the users and revoked_tokens tables. Primary keys and token IDs use ULIDs.
Assets
Assets is a Nexus module. Asset models are authenticated API resources with an ULID id, name, and tracking_type. Tracking type is either serialized or countable.
Assets are authenticated API resources with an ULID id, required tenant_id, required model_id, optional project_id, name, nullable serial_number, quantity, lifecycle status, and lifecycle timestamps. Serialized assets require a serial number and always use quantity 1. Countable assets do not use serial numbers and can store quantities greater than one.
Asset assignments are authenticated API resources with an ULID id, asset_id, either person_id or location_id, start_date, and nullable end_date.
Asset circulations are authenticated API resources with an ULID id, asset_id, circulation_type, optional person_id, optional location_id, occurred_at, and optional notes. Supported circulation types include issuing an asset to a person, returning it, moving it to a location, receiving it at a location, and location confirmation.
Most object list endpoints include records from the selected tenant scope plus shared root-tenant catalog records where those records can be referenced by tenant-owned objects.
Default asset routes:
GET /asset-modelsreturns asset model objects for authenticated requests.POST /asset-modelscreates an asset model withnameandtracking_type.GET /assetsreturns asset objects for authenticated requests.POST /assetscreates an asset withmodel_id, optionalproject_id,name, and eitherserial_numberorquantitydepending on the model tracking type.GET /assets/{asset_id}returns an asset.PATCH /assets/{asset_id}updates an asset.DELETE /assets/{asset_id}deletes an asset.POST /assets/{asset_id}/archivearchives an asset.POST /assets/{asset_id}/restorerestores an asset to active status.POST /assets/{asset_id}/retiremarks an asset retired.POST /assets/{asset_id}/disposemarks an asset disposed.POST /assets/{asset_id}/mark-lostmarks an asset lost.POST /assets/{asset_id}/mark-stolenmarks an asset stolen.GET /assets/{asset_id}/assignmentsreturns assignments for an asset.POST /assets/{asset_id}/assignmentscreates an assignment for an asset.GET /asset-assignmentsreturns asset assignment objects for authenticated requests.POST /asset-assignmentscreates an assignment for a person or location.GET /assignments/{assignment_id}returns an assignment.PATCH /assignments/{assignment_id}updates an assignment.POST /assignments/{assignment_id}/closecloses an assignment.GET /asset-circulationsreturns asset circulation objects for authenticated requests.POST /asset-circulationscreates an asset circulation event.GET /asset-repairsreturns asset repair objects for authenticated requests.POST /asset-repairscreates a parent issue with repair subtype details.
Projects
Projects is a Nexus module. Projects are authenticated API resources with an ULID id, name, optional description, nullable archived_at, and tenant ownership. Projects are the core organizing object for issue work and asset grouping. Issues and assets can be associated to a project through project_id.
The shared tenant seeds shared Technology Service and Maintenance Service projects. Existing non-repair issues are associated to Technology Service during migration, while repair issues are associated to Maintenance Service.
Default project routes:
GET /projectsreturns project objects for authenticated requests.POST /projectscreates a project withnameand optionaldescription.GET /projects/{project_id}returns a project.PATCH /projects/{project_id}updates a project.DELETE /projects/{project_id}deletes a project.POST /projects/{project_id}/archivearchives a project.POST /projects/{project_id}/restorerestores an archived project.GET /projects/{project_id}/groupsreturns groups scoped to a project.GET /projects/{project_id}/issuesreturns issues associated to a project.POST /projects/{project_id}/issuescreates an issue associated to a project.
Projects Issues
Project issues are part of the Projects module. Issues are authenticated API resources with an ULID id, optional project_id, issue_type, title, optional description, priority, status, opened_at, and nullable closed_at. Supported issue subtypes are service requests, known problems, incidents, post-incident analysis, changes, and repairs. Asset repairs are repair subtype resources with an ULID id, parent issue_id, asset_id, optional location_id, and inherited issue title, description, priority, status, reported/opened time, and resolved/closed time.
Configurations
Configurations is a Nexus module.
Configuration items are authenticated API resources with an ULID id, tenant ownership, name, optional ci_type, optional description, and nullable archived_at. Relationships link two configuration items within the same tenant using a relationship_type.
Default project issue routes:
GET /issuesreturns issue objects across issue types.POST /issuescreates an issue based onissue_type.GET /issues/{issue_id}returns an issue.PATCH /issues/{issue_id}updates an issue.DELETE /issues/{issue_id}deletes an issue.POST /issues/{issue_id}/archivearchives an issue.POST /issues/{issue_id}/restorerestores an archived issue.GET /issues/{issue_id}/transitionsreturns available issue transitions.POST /issues/{issue_id}/transitiontransitions an issue.GET /issues/{issue_id}/commentsreturns issue comments.POST /issues/{issue_id}/commentscreates an issue comment.GET /issues/{issue_id}/attachmentsreturns issue attachments.POST /issues/{issue_id}/attachmentscreates an issue attachment reference.GET /issues/{issue_id}/linksreturns issue links.POST /issues/{issue_id}/linkscreates an issue link.GET /issues/{issue_id}/historyreturns issue history.GET /issues/{issue_id}/activityreturns issue activity.GET /issues/{issue_id}/watchersreturns issue watchers.POST /issues/{issue_id}/watchersadds an issue watcher.DELETE /issues/{issue_id}/watchers/{user_id}removes an issue watcher.GET /issue-types,POST /issue-types,GET /issue-types/{issue_type_id},PATCH /issue-types/{issue_type_id}, andDELETE /issue-types/{issue_type_id}manage issue types.GET /issue-statuses,POST /issue-statuses,GET /issue-statuses/{issue_status_id},PATCH /issue-statuses/{issue_status_id}, andDELETE /issue-statuses/{issue_status_id}manage issue statuses.GET /issue-priorities,POST /issue-priorities,GET /issue-priorities/{issue_priority_id},PATCH /issue-priorities/{issue_priority_id}, andDELETE /issue-priorities/{issue_priority_id}manage issue priorities.GET /issue-workflows,POST /issue-workflows,GET /issue-workflows/{issue_workflow_id},PATCH /issue-workflows/{issue_workflow_id}, andDELETE /issue-workflows/{issue_workflow_id}manage issue workflows.GET /issue-workflow-statuses,POST /issue-workflow-statuses,GET /issue-workflow-statuses/{issue_workflow_status_id},PATCH /issue-workflow-statuses/{issue_workflow_status_id}, andDELETE /issue-workflow-statuses/{issue_workflow_status_id}manage workflow statuses.GET /issue-workflow-transitions,POST /issue-workflow-transitions,GET /issue-workflow-transitions/{issue_workflow_transition_id},PATCH /issue-workflow-transitions/{issue_workflow_transition_id}, andDELETE /issue-workflow-transitions/{issue_workflow_transition_id}manage workflow transitions.GET /issue-link-types,POST /issue-link-types,GET /issue-link-types/{issue_link_type_id},PATCH /issue-link-types/{issue_link_type_id}, andDELETE /issue-link-types/{issue_link_type_id}manage issue link types.GET /issue-fields,POST /issue-fields,GET /issue-fields/{issue_field_id},PATCH /issue-fields/{issue_field_id}, andDELETE /issue-fields/{issue_field_id}manage issue fields.GET /issue-screens,POST /issue-screens,GET /issue-screens/{issue_screen_id},PATCH /issue-screens/{issue_screen_id}, andDELETE /issue-screens/{issue_screen_id}manage issue screens.GET /issue-screen-fields,POST /issue-screen-fields,GET /issue-screen-fields/{issue_screen_field_id},PATCH /issue-screen-fields/{issue_screen_field_id}, andDELETE /issue-screen-fields/{issue_screen_field_id}manage issue screen fields.
Default configuration routes:
GET /configuration-itemsreturns configuration item objects for authenticated requests.POST /configuration-itemscreates a configuration item.GET /configuration-items/{ci_id}returns a configuration item.PATCH /configuration-items/{ci_id}updates a configuration item.DELETE /configuration-items/{ci_id}deletes a configuration item.POST /configuration-items/{ci_id}/archivearchives a configuration item.POST /configuration-items/{ci_id}/restorerestores an archived configuration item.GET /configuration-items/{ci_id}/relationshipsreturns relationships for a configuration item.POST /configuration-items/{ci_id}/relationshipscreates a relationship from a configuration item.DELETE /relationships/{relationship_id}deletes a configuration item relationship.
Locations
Locations is a Nexus module. Location types are authenticated API resources with an ULID id and name.
Locations are authenticated API resources with an ULID id, nullable parent_id, name, and required type_id.
Default location routes:
GET /location-typesreturns location type objects for authenticated requests.POST /location-typescreates a location type withname.GET /locationsreturns location objects for authenticated requests.POST /locationscreates a location withparent_id,name, andtype_id.
Census
Census is a Nexus module. Persons are authenticated API resources with an ULID id, nullable current_identity_id, and nullable archived_at.
Identities are authenticated API resources with an ULID id, person_id, last_name, first_name, optional middle_name, nullable deactivated_at, and nullable archived_at. Contacts store person or identity contact details with type, value, optional label, and sort order.
Default Census routes:
GET /personsreturns person objects for authenticated requests.POST /personscreates a person.GET /persons/{person_id},PATCH /persons/{person_id}, andDELETE /persons/{person_id}manage a person.POST /persons/{person_id}/archivearchives a person.POST /persons/{person_id}/restorerestores an archived person.GET /persons/{person_id}/current-identityreturns the current identity for a person.POST /persons/{person_id}/current-identitysets the current identity for a person.GET /persons/{person_id}/identitiesandPOST /persons/{person_id}/identitieslist and create identities for a person.GET /persons/{person_id}/identities/{identity_id},PATCH /persons/{person_id}/identities/{identity_id}, andDELETE /persons/{person_id}/identities/{identity_id}manage a person identity.POST /persons/{person_id}/identities/{identity_id}/activateandPOST /persons/{person_id}/identities/{identity_id}/deactivatemanage identity activation.POST /persons/{person_id}/identities/{identity_id}/archiveandPOST /persons/{person_id}/identities/{identity_id}/restoremanage identity archival.GET /identities,GET /identities/{identity_id}, andPATCH /identities/{identity_id}provide identity lookup.GET /persons/{person_id}/contacts,POST /persons/{person_id}/contacts,GET /persons/{person_id}/contacts/{contact_id},PATCH /persons/{person_id}/contacts/{contact_id}, andDELETE /persons/{person_id}/contacts/{contact_id}manage person contacts.PUT /persons/{person_id}/contacts/orderupdates person contact order.GET /persons/{person_id}/identities/{identity_id}/contactsandPOST /persons/{person_id}/identities/{identity_id}/contactsmanage identity contacts.GET /contacts,GET /contacts/{contact_id},PATCH /contacts/{contact_id}, andDELETE /contacts/{contact_id}provide contact lookup.
Education
Education is a Nexus module. Schools are authenticated API resources with an ULID id, tenant ownership, name, and nullable archived_at.
School years are authenticated API resources with an ULID id, tenant ownership, name, start, end, nullable archived_at, and is_current.
School calendars are authenticated API resources with an ULID id, school_id, calendar_id, and name. The calendar_id references a school year.
School enrollments are authenticated API resources with an ULID id, person_id, calendar_id, status, and lifecycle timestamps. The calendar_id references a school calendar.
Education routes:
GET,POST,GET by id,PATCH, andDELETEmanage schools, school years, school enrollments, school employments, school employment assignments, school grade levels, and employment assignment types.POST /schools/{school_id}/archiveandPOST /schools/{school_id}/restoremanage school lifecycle.POST /school-years/{school_year_id}/archive,POST /school-years/{school_year_id}/restore, andPOST /school-years/{school_year_id}/make-currentmanage school year lifecycle.GET /schools/{school_id}/calendars,POST /schools/{school_id}/calendars, andGET /school-years/{school_year_id}/calendarsexpose calendar relationships.POST /school-enrollments/{school_enrollment_id}/withdraw,/reenroll,/graduate, and/transfermanage enrollment status.GET /persons/{person_id}/school-enrollments,GET /school-calendars/{school_calendar_id}/enrollments,POST /school-calendars/{school_calendar_id}/enrollments,GET /schools/{school_id}/enrollments, andGET /school-years/{school_year_id}/enrollmentsexpose enrollment scopes.POST /school-employments/{school_employment_id}/hire,/terminate,/reactivate, and/retiremanage employment status.POST /school-employment-assignments/{assignment_id}/start,/end, and/reactivatemanage assignment status.GET /school-employments/{school_employment_id}/assignments,POST /school-employments/{school_employment_id}/assignments,GET /schools/{school_id}/employment-assignments,GET /school-calendars/{school_calendar_id}/employment-assignments, andGET /persons/{person_id}/school-employment-assignmentsexpose assignment scopes.