Document Version: 1.0
Last Updated: January 2026
Purpose: Technical debt inventory, refactoring priorities, architecture improvements, migration strategies
This roadmap outlines a phased approach to modernizing the Nightscout codebase while maintaining stability for the existing user base. The focus is on improving accuracy, velocity (development speed), and maintainability.
- Backward Compatibility: Maintain API compatibility during transitions
- Incremental Progress: Small, testable changes over big rewrites
- User Safety First: Health-critical application - no regressions
- Community Involvement: Open source project requires consensus
| Item | Location | Risk | Effort |
|---|---|---|---|
Deprecated request library |
Multiple files | Security | Medium |
| No input validation | API endpoints | Security | High |
| No rate limiting | Server | DoS vulnerability | Medium |
| Outdated Node.js support | package.json | Security | Low |
| Item | Location | Impact | Effort |
|---|---|---|---|
| Callback-based async code | Throughout | Maintainability | High |
| No TypeScript | Throughout | Developer velocity | Very High |
| Global state (ctx object) | Server code | Testability | High |
| jQuery dependency | Client | Bundle size, modernization | High |
| Moment.js bundle size | Client | Performance | Low |
| Item | Location | Impact | Effort |
|---|---|---|---|
| No database migrations | Storage | Operations | Medium |
| Inconsistent error handling | API layers | Debugging | Medium |
| Missing test coverage | Throughout | Quality | High |
| No structured logging | Server | Observability | Medium |
| Mixed module systems | Throughout | Build complexity | Medium |
| Item | Location | Impact | Effort |
|---|---|---|---|
| D3.js v5 (outdated) | Client | Features | Medium |
| Flot charts (legacy) | Reports | Maintainability | Medium |
| Manual DOM updates | Client | Complexity | High |
| No service worker | Client | Offline/PWA | Medium |
Goal: Address critical security issues and establish modern tooling
Effort: Low to Medium | Complexity: Straightforward
Action: Replace request library with axios
// Before
var request = require('request');
request.post({ url, json }, callback);
// After
const axios = require('axios');
await axios.post(url, json);Files Affected:
lib/plugins/maker.jslib/plugins/pushover.jslib/plugins/bridge.jslib/server/bootevent.js
Effort: Low | Complexity: Straightforward (find-and-replace pattern)
Action: Require Node.js 18 LTS minimum
{
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
}
}Effort: Low | Complexity: Straightforward (config change only)
Action: Implement Zod schemas for API endpoints
const { z } = require('zod');
const entrySchema = z.object({
type: z.enum(['sgv', 'mbg', 'cal']),
sgv: z.number().int().min(20).max(600).optional(),
date: z.number().int().positive(),
direction: z.string().optional()
});
// Middleware
function validateBody(schema) {
return (req, res, next) => {
try {
req.body = schema.parse(req.body);
next();
} catch (err) {
res.status(400).json({ error: err.issues });
}
};
}Effort: Medium | Complexity: Moderate (many endpoints, testing needed)
Action: Implement express-rate-limit
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
standardHeaders: true,
legacyHeaders: false
});
app.use('/api/', apiLimiter);Effort: Low | Complexity: Straightforward (middleware addition)
Action: Replace console.log with pino
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info'
});
// Usage
logger.info({ event: 'data_update', entries: count }, 'Data updated');
logger.error({ err, endpoint }, 'Request failed');Effort: Medium | Complexity: Straightforward (systematic replacement)
Goal: Improve developer productivity and code quality
Effort: High | Complexity: Moderate to Complicated
Strategy: Incremental adoption using JSDoc + TypeScript checking
Step 1: Add tsconfig.json with allowJs
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true,
"target": "ES2020",
"module": "CommonJS",
"strict": false
},
"include": ["lib/**/*"]
}Step 2: Add type definitions for core modules
// types/context.d.ts
interface NightscoutContext {
bus: EventEmitter;
ddata: DataStore;
plugins: PluginManager;
notifications: NotificationManager;
// ...
}Step 3: Convert files incrementally (.js → .ts)
Effort: High | Complexity: Moderate (incremental, ongoing)
Strategy: Convert callback-based code to async/await
// Before
function setupStorage(ctx, next) {
require('../storage/mongo-storage')(env, function(err, store) {
if (err) {
ctx.bootErrors.push({ err });
}
ctx.store = store;
next();
});
}
// After
async function setupStorage(ctx) {
try {
ctx.store = await require('../storage/mongo-storage')(env);
} catch (err) {
ctx.bootErrors.push({ err });
}
}Priority Files:
lib/server/bootevent.jslib/authorization/index.jslib/api3/endpointslib/data/dataloader.js
Effort: Medium | Complexity: Moderate (requires understanding callback patterns)
Action: Expand test coverage
// Example: API endpoint tests
describe('GET /api/v3/entries', () => {
it('should return entries for authorized user', async () => {
const response = await request(app)
.get('/api/v3/entries')
.set('Authorization', `Bearer ${validToken}`)
.expect(200);
expect(response.body.status).toBe(200);
expect(response.body.result).toBeInstanceOf(Array);
});
it('should reject unauthorized requests', async () => {
await request(app)
.get('/api/v3/entries')
.expect(401);
});
});Coverage Targets:
- API endpoints: 80%
- Plugin logic: 70%
- Authorization: 90%
- Data processing: 75%
Effort: High | Complexity: Moderate (ongoing effort)
Goal: Improve client-side performance and user experience
Effort: Medium | Complexity: Moderate
Actions:
- Replace Moment.js with dayjs:
// Before
const moment = require('moment-timezone');
moment(date).format('HH:mm');
// After
const dayjs = require('dayjs');
const utc = require('dayjs/plugin/utc');
const timezone = require('dayjs/plugin/timezone');
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs(date).format('HH:mm');Size Reduction: ~200KB
- Tree-shake Lodash:
// Before
const _ = require('lodash');
_.debounce(fn, 1000);
// After
import debounce from 'lodash-es/debounce';
debounce(fn, 1000);Size Reduction: ~50KB
- Code Splitting (Webpack Dynamic Imports):
// Lazy load reports module using Webpack dynamic imports
// This works with the existing jQuery/D3 architecture
function loadReportsModule() {
return import(/* webpackChunkName: "reports" */ './reports').then(module => {
return module.default;
});
}
// Usage: Load reports only when needed
$('#reports-tab').on('click', async function() {
const reports = await loadReportsModule();
reports.init(client);
});Effort: Low to Medium | Complexity: Straightforward (library swaps + config)
Actions:
- Add Service Worker:
// service-worker.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('nightscout-v1').then((cache) => {
return cache.addAll([
'/',
'/bundle/bundle.js',
'/bundle/bundle.css'
]);
})
);
});- Add Web Manifest:
{
"name": "Nightscout",
"short_name": "NS",
"start_url": "/",
"display": "standalone",
"theme_color": "#000000"
}Effort: Low | Complexity: Straightforward (add new files)
Strategy: Incremental replacement
// Before
$('#currentBG').text(bg);
$('#container').addClass('urgent');
$('.pill').on('click', handler);
// After
document.getElementById('currentBG').textContent = bg;
document.getElementById('container').classList.add('urgent');
document.querySelectorAll('.pill').forEach(el => {
el.addEventListener('click', handler);
});Effort: High | Complexity: Complicated (incremental, many touch points)
Goal: Improve scalability and maintainability
Effort: High | Complexity: Complicated
Action: Replace Stream-based bus with typed EventEmitter
// lib/bus.ts
import { EventEmitter } from 'events';
interface BusEvents {
'tick': (tick: TickEvent) => void;
'data-received': () => void;
'data-loaded': () => void;
'notification': (notify: Notification) => void;
'teardown': () => void;
}
class TypedEventBus extends EventEmitter {
emit<K extends keyof BusEvents>(event: K, ...args: Parameters<BusEvents[K]>): boolean {
return super.emit(event, ...args);
}
on<K extends keyof BusEvents>(event: K, listener: BusEvents[K]): this {
return super.on(event, listener);
}
}Effort: Medium | Complexity: Moderate (contained refactor)
Action: Implement proper migrations using migrate-mongo
// migrations/20260101-add-identifier-index.js
module.exports = {
async up(db) {
await db.collection('entries').createIndex({ identifier: 1 });
},
async down(db) {
await db.collection('entries').dropIndex('identifier_1');
}
};Effort: Low | Complexity: Straightforward (new tooling, minimal code changes)
Action: Add OpenID Connect and OAuth2 support as a plugin for vendor-agnostic identity
Rationale:
- Delegate identity complexity to purpose-built tools
- Keep Nightscout focused on CGM data handling
- Enable integration with enterprise identity providers
- Support consent and delegation workflows
Architecture:
┌─────────────────────────────────────────────────────────────────────────┐
│ IDENTITY LAYER (External) │
├─────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Ory Hydra │ │ Ory Kratos │ │ Other IdPs │ │
│ │ (OAuth2/OIDC) │ │ (Identity Mgmt) │ │ (Okta, Auth0) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ └──────────────────────┼──────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ nightscout-roles-gateway │ │
│ │ (Consent & Delegation) │ │
│ │ github.com/t1pal/... │ │
│ └──────────────┬──────────────┘ │
└───────────────────────────────────┼─────────────────────────────────────┘
│ OIDC claims → NS permissions
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ NIGHTSCOUT │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ OIDC Plugin (lib/plugins/oidc.js) │ │
│ │ - Validate OIDC tokens │ │
│ │ - Map claims to Shiro permissions │ │
│ │ - Coexist with existing API_SECRET auth │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Implementation Approach:
// lib/plugins/oidc.js
const { Issuer } = require('openid-client');
async function init(env, ctx) {
const issuer = await Issuer.discover(env.OIDC_ISSUER_URL);
const client = new issuer.Client({
client_id: env.OIDC_CLIENT_ID,
client_secret: env.OIDC_CLIENT_SECRET
});
// Middleware to validate OIDC tokens
ctx.authorization.addTokenValidator('oidc', async (token) => {
const userinfo = await client.userinfo(token);
return mapClaimsToPermissions(userinfo);
});
}Effort: Medium | Complexity: Moderate (well-defined protocol, existing libraries)
Action: Support multiple data holders through nightscout-roles-gateway
Reference: https://github.com/t1pal/nightscout-roles-gateway
Capabilities:
- Consent Management: Data holder controls who can access their data
- Delegation: Caregivers, clinicians, AI agents with scoped permissions
- Authority Hierarchy: Aligns with Control Plane RFC (Human > Agent > Controller)
- Audit Trail: Who accessed what data, when, with what permissions
Integration Points:
// Gateway handles:
// 1. User authentication via Ory Kratos
// 2. OAuth2 consent flows via Ory Hydra
// 3. Permission mapping to Nightscout Shiro permissions
// 4. Multi-tenant routing (optional)
// Nightscout receives:
// - Standard OIDC token with claims
// - Claims include: subject_id, permissions[], delegated_by, expires_atBenefits over Internal Implementation:
- Separation of concerns (identity vs. CGM data)
- Proven identity infrastructure (Ory stack)
- Standards-compliant (OAuth2, OIDC)
- Easier security audits (smaller attack surface in Nightscout)
Effort: Medium | Complexity: Moderate (integration work, minimal Nightscout changes)
Goal: Modern, responsive, accessible user interface
Effort: Very High | Complexity: Complicated
Recommendation: Vue.js or Svelte for incremental migration
Vue.js Strategy:
- Create Vue components for new features
- Mount Vue components alongside existing DOM
- Gradually replace jQuery-based UI
// Mount Vue component in existing app
import { createApp } from 'vue';
import StatusPills from './components/StatusPills.vue';
createApp(StatusPills).mount('#status-pills');Effort: Very High | Complexity: Complicated (major architecture shift)
Actions:
- Add ARIA labels
- Implement keyboard navigation
- Add screen reader announcements
- Improve color contrast
- Add focus indicators
<!-- Before -->
<div class="pill" onclick="ack()">120</div>
<!-- After -->
<button
class="pill"
role="button"
aria-label="Current blood glucose: 120 mg/dL. Press to acknowledge."
tabindex="0"
onclick="ack()"
onkeypress="if(event.key==='Enter')ack()">
120
</button>Effort: Medium | Complexity: Moderate (systematic, well-defined)
Option 1: Upgrade D3.js to v7
Option 2: Consider Chart.js for simpler charts
Option 3: Custom WebGL-based chart for performance
Effort: Medium to High | Complexity: Moderate to Complicated (depends on option chosen)
For major subsystems, wrap old code and redirect gradually:
// Phase 1: Wrapper
async function getEntries(query) {
if (useNewImplementation()) {
return newEntriesService.get(query);
}
return oldEntriesAPI.get(query);
}
// Phase 2: Migrate traffic
// Phase 3: Remove old codeconst features = {
USE_NEW_AUTH: process.env.FEATURE_NEW_AUTH === 'true',
USE_REDIS_CACHE: process.env.FEATURE_REDIS_CACHE === 'true',
USE_VUE_COMPONENTS: process.env.FEATURE_VUE === 'true'
};
if (features.USE_NEW_AUTH) {
app.use('/api', newAuthMiddleware);
} else {
app.use('/api', legacyAuthMiddleware);
}For critical subsystems (notifications), run old and new in parallel:
async function sendNotification(notify) {
// Run both, compare results
const [oldResult, newResult] = await Promise.all([
oldNotificationSystem.send(notify),
newNotificationSystem.send(notify)
]);
if (oldResult !== newResult) {
logger.warn({ oldResult, newResult }, 'Notification mismatch');
}
return oldResult; // Use old until validated
}| Change | Risk | Mitigation |
|---|---|---|
| Auth refactoring | Users locked out | Feature flag, gradual rollout |
| Notification changes | Missed alerts | Parallel running, extensive testing |
| Data layer changes | Data loss/corruption | Comprehensive backups, migrations |
| API changes | Breaking clients | Version compatibility, deprecation |
| Phase | Test Coverage | Type |
|---|---|---|
| Phase 1: Security Foundation | 80%+ | Unit, integration |
| Phase 2: Developer Experience | 75%+ | Unit, E2E |
| Phase 3: Performance & UX | 70%+ | Performance, visual |
| Phase 4: Architecture | 80%+ | Load, chaos |
| Phase 5: UI Modernization | 70%+ | Accessibility, E2E |
- Database: Maintain migration rollback scripts
- API: Version headers, backwards compatibility
- Client: Serve multiple bundle versions
- Features: Feature flags for instant rollback
| Phase | Effort | Complexity | Team Size |
|---|---|---|---|
| Phase 1: Security Foundation | Low to Medium | Straightforward | 1-2 developers |
| Phase 2: Developer Experience | High | Moderate to Complicated | 2 developers |
| Phase 3: Performance & UX | Medium | Moderate | 1-2 developers |
| Phase 4: Architecture | High | Complicated | 2 developers |
| Phase 5: UI Modernization | Very High | Complicated | 2-3 developers |
| Item | Requirement | Cost Estimate |
|---|---|---|
| Redis | Production Redis | ~$50-200/month |
| CI/CD | GitHub Actions | Free (open source) |
| Monitoring | Datadog/Grafana | ~$0-100/month |
| Load Testing | k6/Artillery | Free |
| Metric | Current | Target |
|---|---|---|
| Notification delivery rate | Unknown | >99.9% |
| Data consistency errors | Unknown | <0.01% |
| API error rate | Unknown | <0.1% |
| Metric | Current | Target |
|---|---|---|
| Time to deploy | Manual | <10 min |
| Test suite runtime | ~5 min | <3 min |
| New developer onboarding | ~1 week | ~2 days |
| Metric | Current | Target |
|---|---|---|
| Code coverage | ~40% | >75% |
| TypeScript coverage | 0% | >60% |
| Documentation | Partial | Comprehensive |
| Dependency age | Mixed | <1 year |