pebble-docs

PebbleSRV Docs

Overview
PebbleUI

About PebbleSRV

# PebbleSRV is Spacefix's proprietary backend application framework — a batteries-included, opinionated server runtime for building and scaling modern NestJS applications.

# Built heavily around Mongoose and MongoDB, it goes beyond a module library by standardizing how backend services are structured, configured, secured, and composed. PebbleSRV provides a single, well-maintained foundation that bundles authentication, payment processing, file storage, notifications, email delivery, database transactions, pagination, and enhanced NestJS primitives into a cohesive runtime teams can reuse across services.

# By centralizing architectural patterns and complex integrations once, PebbleSRV reduces duplication, improves consistency, and allows teams to focus on business logic instead of reinventing backend infrastructure.

Maintenance Guide

# Below is the Modules Heirarchy (Descending order)

# EntitiesModule,

# App-ConfigModule, ErrorsModule

# CryptoModule,

# OTPModule, FileStorageModule, DB-TransactionModule, AbilityModule, EmailModule, PaymentProcessorModule

# (Mainly Utils Modules):
# AuthSessionMoudle, Auth-Utils,

# AuthModule
# ConferencingModule,

# (Mainly Domain Modules):
# FreelancePractitionersModule
# UsersModule
# OrgsModule
# MedConsultProvidersModule
# MedConsultOrdersModule
# MedConsultProfilesModule

# PaymentsModule
# AppNotificationsModule


# The utils, enums, constants etc needed by BOTH EntitiesModule and the rest of the app are in the entities folder
# The lib folder inside entities folder contains all the items needed by ONLY the rest of the app

# root
# index.ts

Services and Products

# The actual services are not strict. This is because we don't have a way of identifying ahead of time all possible services that healthsenta accepts
# Products and Services schema and listing definitions interfaces and tree share a similar flow strongly, up till service and product props in their respective schema.

File Uploads

# Make all file-related fields optional in the DB to handle potential upload failures from the third-party hosting server:
# If an upload fails, the required file data won't be available when saving the rest of the payload, causing a DB error if the fields are mandatory. Given the complexity of our payload manipulation when handling files, reliably managing failures and ensuring smooth rollback or reconciliation can be challenging during payload processing; so there is a high risk of inconsistencies if the file fields are required in the DB. A more practical approach is to allow failed uploads to remain as such (and they no longer have to be uploaded to our DB something the DB is at peace with since the file-related fields are optional), and provide a way to update file-related fields later, e.g., via records editing.

# We deal absolutely with lowercase only when manipulating, storing or retrieving file formats (or file extensions)

Roles and Permissions

# Only org owners can manage Roles and Permissions.
# Internal org owner is always present.

Date, DateTime, and Time

# When interested in only date:
# - Schema prop type is Date
# - Dto prop is:
#  @IsDefined()
#  @IsDate()
#  @Type(() => Date)
#  date: Date;
# - Frontend MUST send an ISO 8601 string in UTC
# - prop name can be date or suffixed with Date

# When interested in date and time as complimentary flow (like an exact instant or moment in time e.g., say a meeting date and time):
# - Schema prop type, Dto prop, and Frontend format is exactly as above for only date.
# - prop name can be dateTime or suffixed with DateTime

# When interested in only time (HH:mm or HH:mm:ss) as string:
# - Schema prop type is String with the `match` value of the constant `time24HourRegex` (e.g., TimeSlotProp start and end)
# - Dto prop is the custom @IsTimeString()
# - Frontend MUST send just a 'HH:mm' or 'HH:mm:ss' formatted date (dateAdapter()) string

# In other instances the normal Date (exactly as discussed above) can be used for Time. But in such cases, consumer must always note to ignore the date part of the object and focus on the time part when performing any calculations or displaying the value.

# SIDE NOTES:
# Part A
# A normal JS Date object should always be passed over to the DB. Gladly, we don't have to worry about this as our dto handles that. But if any manipulation takes place in the services (after controller executes dto), always make sure that a normal JS Date instance created either off of an ISO 8601 string in UTC (or with offset) or Epoch milliseconds (number) is passed on to the DB.

# Part B:
# Instances where newing up a JS Date object might cause inconsistencies or non-standard parsing. Parsing will then be based on the server machines or engines. Below are some of those instances:
# 1. Local timezone effects - If the input has no timezone info (e.g., "2025-08-15T12:00:00" without Z or offset), ECMAScript treats it as local time on that system.
# 2. Date-only strings - A bare "YYYY-MM-DD" is also interpreted as midnight in local time, then converted to UTC internally
# 3. If the input is not ISO 8601 or epoch milliseconds — e.g., "08/15/2025"
# 4.  OS & locale dependencies - If a format is locale-sensitive, the system’s ICU build and locale settings can subtly change parsing.
# These can be avoided if the input is an ISO 8601 string in UTC (or with offset) or Epoch milliseconds (number)

Med Consults Booking System

# Key Assumptions of our Med Consult Booking System (conditions under which accuracy is guaranteed):
# 1. Booking duration must never exceed 24 hours (in Mins); but best at ≤ 6 hours for optimal performance.
# 2. Booking time steps or duration time steps must be in minutes of 5s; say 10:00, 10:05, 10:10 and so on.

# Two collections work hand-in-hand to guarantee accuracy for the Med Consult Booking system: The MedConsultOrders and MedConsultOrderSlotReservations collections. If any of this is tampered with, the Med Consult system algorithm will be compromised.

# When a provider wants to change timzeZone, sessionDurationInMins, availability, make sure no booking currently exists for them

# When we might suffer performance bottleneck (with the atomic slot reservation and order creation flow in mongodb transaction; essential however to mitigate overbooking) algorithm we have:
# - Longer booking duration; say > 6 hours
# - Many simultaneous attempts for the same slot window, same med consult type, and same provider
# - Generally speaking when traffic on the Med Consult Platform is above moderate.

# Situations that might compromise the reliability of the Booking accuracies of customers for a given provider:
# - Whenever a provider is performing an update operation (involving any or all of timzeZone, sessionDurationInMins, availability) and concurrently (or about the same time) a customer(s)'s booking request slipped through the provider's isActive check when booking med consults - now if it happens that the customer(s) request gets inside this zone AND the customer's booking did not complete before the updating provider's update request completes the provider's atomic check-and-lock provider's profile if-no-booking phase; that/those customer(s) will be booking based on the provider's previous profile which may no longer be true once the provider's updates are successful. More or less, it is safe to say provider updated those sensitive fields while he had existing bookings. It will compromise the entire Booking calender tracking and blocking for this provider and his customers. This applies accordingly regardless of the type of Med Consult that is affected.

# - Now we can try to solve the above by introducing more engineering, but what we currently have is the perfect balance between performance and complexity - anything above that will be much more complex and hurt performance a lot. It is howerver necessary only if later it becomes as such!

Forked Libraries (for Update Tracking)

# This list is important cos it helps us keep track of libraries that were forked so that the latest updates of such libraries can always be tracked and effected manually. They include the following:

# 1. http-terminator and lil-http-terminator (Both are for Gracefully terminating HTTP(S) server) used in http-terminator.ts
# 2. https://www.npmjs.com/package/mongoose-paginate-v2 used in paginate.ts

Error Logging and Propagation Strategy

# 1. Intermediate Errors:
# Errors that occur during processing (e.g., inside a pipeline or batch operation) that should not terminate the overall process should be logged with context right where and when they occurred for tracking purposes while the remainder of such a process continues. Such errors cannot be allowed to throw to avoid distrupting the entire process invloved.

# 2. Final Errors:
# Errors that occur in a process where failure means the process cannot continue or complete meaningfully, should be allowed to throw as-is - this will bubble up into the AppExceptionFilter where logging is already configured automatically. Such errors do not need to be handled where they throw - just let it bubble up and the Application will handle it gracefully, automatically.

Dealing With JavaScript Type number


# ==========================================
# BACKGROUND UNDERSTANDING
# ==========================================

# JavaScript Number is an IEEE-754 double (64-bit floating point) which has precision limits:

# 1. Integers are only safe up to ±(2^53 − 1) ≈ 9,007,199,254,740,991; Beyond this, JS cannot distinguish consecutive integers reliably.

# 2. A Number has ~15–17 significant decimal digits of precision TOTAL (shared between integer and fractional parts).
# Examples:
# 0.1 + 0.2 → 0.30000000000000004 (floating-point error)
# 1.234567890123456 → exact
# 1.23456789012345678 → rounded (extra digits lost)

# NOTE: Both the safe significant decimal places AND the safe value must be respected for reliable precision.

# 3. The larger the integer part, the fewer fractional digits can be preserved. Example: 9007199254740991.123 → decimals dropped (becomes 9007199254740991)

# Quick Reference (safe max fractional digits by integer size):
# ≤ 1 digit integer → ~15–16 decimal places safe
# ≤ 6 digit integer → ~10–11 decimal places safe
# ≤ 9 digit integer → ~7–8 decimal places safe
# ≤ 12 digit integer → ~4–5 decimal places safe
# ≤ 15 digit integer → ~1–2 decimal places safe
# > 15 digit integer → decimals unsafe (will be truncated)

# Summary:
# Safe integers: up to 9 quadrillion (~10^15) and not exceeding ± 9,007,199,254,740,991
# Safe decimals: about 15–17 total significant digits (integer + fraction combined)
# Use libraries (e.g., Big.js, Decimal.js) for calculations that must exceed these limits.

# Safe ranges of value that can be stored with number for fixed decimal places (approx):
# 2 dp → up to ~90 trillion (9.0e13) - this is the max storable amount value you get with money (if storing to a max of 2 dp which is typically very okay for all major currencies. It also covers the Japanese YEN which has no d.p. by the way, even better!
# 3 dp → up to ~9 trillion (9.0e12)
# 4 dp → up to ~900 billion (9.0e11)
# 6 dp → up to ~9 billion (9.0e9)
# If the value expected for number type cannot fall in this range for the specified decimal places, then number must not be used to store such values. Use Decimal128 to store such on MongoDB. Refer to the Decimal128 part of this documentation for the specifics when dealing with such cases.

# See the folders entities > _lib > precise-decimal-adapter.ts file to see the utilities developed for this documentation.


# ==========================================
# IMPLEMENTATION SPECIFICS
# ==========================================


# STORING VALUES AS NUMBER
# ==========================================

# JavaScript uses IEEE 754 double-precision floating-point numbers. Because of this, numbers with decimal values like 0.1 and 0.2 cannot be represented exactly in binary. E.g., 0.1 + 0.2 = 0.30000000000000004; not 0.3. This leads to precision issues when working with decimal numbers, particularly involving comparison operators ===, <, <=, >, >=

# These issues are often experienced when dealing with money amounts, other kinds of amounts like quantities and measurements, discounts, commissions, rates or variables involving the JS number type with potential decimal values, generally.

# To work around this issues reliably for most practical real world situations (EXCLUDING situations like say with Scientific calculations where absolute precision is vital), we have outlined the following workflows:

# 1. All number comparison operator checks must be done using epsilon - this way we get to define our domain tolerance range and work safely within such range during checks. We define the helper operators for each operator check (using epsilon internally) and use this helpers to perform every numeric comparison checks accordingly.

# 2. Every mathematical computations with potential for decimal values must be done with a chosen arbitrary-precision library to ensure computational floating precision. Generally, use epsilon comparison in the precision library calculations when comparing the precision library results with external float values or comparing external float values in the precision library calculations, or when validating against expected ranges. While, you use strict precision library comparison when comparing two precision library calculations or final audit trails.

# NOTE: Because these intermediate calculations can get unpredictably large likely hitting the JS Number Precision limits and causing precision issues, we let a Pure arbitrary-precision library (Big.js, Decimal.js, or Decimal.js-light) handle all intermediate calculations and finish up with the result. If the results are understood ahead of time to be within our reliable precise storable number limit (as can always be figured out ahead of time) we can reliably convert to number and store it. If the result is understood to likely exceed the precision limits when converted to number then such result should not be converted to number from the arbitrary-precision result, neither should it be stored at all as number. Decimal128 and string type will be the right way to store and manage such a value. For specifics on working with this pattern, refer to the Decimal128 part of this documentation.

# 3. Number values that can be expressed and worked out in integers should as much as possible be handled (and MUST be stored up in MongoDB) as integers to keep them from floating. This ensures accuracy since integers for nearly all real world practical cases do not face precision issues.

# 4. Most importantly, all floating point values MUST be stored in the DB in very exact decimal places (this increases predictability since MongoDB stores exactly whatever decimal value JavaScript gives it). The best way to achieve this is to use a system floating value normalization helper function - this function ensures that a fixed number of decimal places is always stored in the DB for any floating point value passed. WE CAN KEEP THIS AT THE SETTER LEVEL of the mongoose schema for the target props. And if for any reason (rarely) the setters fail on update operations, use the {runSettersOnQuery: true} flag in the upate options object.

# Actually, provided normalized values (type number) are always stored and retrieved as-is, then we can safely perform comparison operators reliably between such values without the need for epsilon.

# 5. Recommended system defaults:
# A. Default normalization number of Decimal Places for all floating point values: 4 d.p. (Max storable value at 4 d.p. is up to ~900 billion) - which is fine for our current non-money amount fields.
# B. Recommended arbitrary-precision library: Big.js

# - Reasons for A: Handles most real-world scenarios - Gas prices ($3.599 → $3.5990), Exchange rates (1.2347 EUR/USD), Basis points in finance. When 4 d.p. Isn't Enough - Cryptocurrency trading (need 8 decimals), High-frequency trading (need 6+ decimals), Scientific computing (need 10+ decimals), Very specialized financial instruments. For the situations where 4 d.p. is not enough - fall to Decimal128 for storing such values.

# - Reasons for B:
# Big.js: Basic math requirement, small bundle size, pure and reliable arbitrary-precision handling.
# Decimal.js (or Decimal.js-light preferrably due to smaller bundle size) - Only for advanced math or >$90T values. Even then decimal.js has more advanced math capabilities over decimal.js-light.

# STORING VALUES AS DECIMAL128
# ==========================================

# JavaScript uses IEEE 754 double-precision floating-point numbers. Because of this, numbers with decimal values — or those that may be involved in arithmetics producing decimals which are likely to exceed the JS Number precision limits should not be stored/managed as raw JS number type due to precision loss.

# Recommended Approach:
# - Use string representations of numbers in combination with pure arbitrary-precision library for precise arbitrary-precision computations.
# - In MongoDB (with Mongoose ODM), prefer the Decimal128 BSON type. This preserves values as strings internally, while still supporting numeric queries and operators.

# Key Notes:
# - Decimal128 values are live objects that serialize over the wire (on the Client) to { "$numberDecimal": "…" }. To make them easier to work with on the client, we define schema-level getters for Decimal128 fields. These getters return plain strings instead of objects. get: (v: any) => v?.toString()
# - Performance caveat: getters add overhead when fetching large result sets. We’ve tested and confirmed that up to 50 documents per page (our frontend’s max page size) with multiple Decimal128 fields performs reliably.

# Thanks to the getter, accessing a Decimal128 field in app code yields a ready-to-use string. This makes integration with arbitrary-precision library for computations straightforward. Mongoose will also auto-cast a string assigned to a Decimal128 schema field back into Decimal128 when writing to the database.

# NOTES:
# 1. When working wih Decimal128, stay totally away from number or any of the utilities we built for it; You store, retrieve, compare and compute strictly in string. Both within or outside the arbitray-precision library.
# 2. Use Decimal128 for field whose value amonst other things can have decimal places greater than 4.

PebbleSRV Key Notes

# DESIGN SUMMARY
# Instead of exporting everything from root, we exposed subpaths exports in the package.json (Multi-entry build)
# We support tree-shaking
# The framework is ESM-only (No CJS support cos this is not needed at all).

# FRAMEWORK EXPORT RULES:
# If a folder has any resource at all that needs to be exported from the framework, every content of such a folder must be consumed (internally/externally) via an index.ts file. This will enlists (or exports) the public resources of such a folder. Note that such a folder must be designed to be "self-sustaining" - A folder is self containing if it is the most granular unit importable without the possibility of bi-directional or circular dependency issue). The files or components within a folder importing each other should be done without using the index.ts entry of such a folder - they should import directly, while making sure no possibility of bi-directional dependency.

# If no resource of a folder needs to be exported from the framework, then index.ts export file is not used, content of the folder are imported using their relative absolute paths. This is basically to reduce the population of files that are likely to contribute to circular dependency issues.

# Any individual file that is self-sustaining (e.g., Single component files that do not fit into any suitable "self-sustaining" folder) basically exports itself either as a default or named export.

# The framework rely strictly on @PublicEntry as the single source of truth for PebbleSRV exports. PebbleSRV exports are files whose content can be imported by consuming Apps of the framework. Any individual file or barrel export (e.g., index.ts) file that must be exported from the framework must have the marker /** @PublicEntry */ at the topmost layer of the file.

# You can use the typescript /** @internal */ marker to hide individual functions, methods, or interfaces from consuming Apps even if the file containing them is marked /** @PublicEntry */.

# TODO:: Document how Notifications work for the consuming App (For payment, each kind of payment has it own file - which contains it payments and it payouts, together with other necessary methods from payout webhook and notifications)

#TODO:: Document how Webhook for consuming App