top of page

How RxDb is helping us

  • brandonwilson35
  • Jul 5
  • 4 min read

When we started building ACE, we knew construction crews needed to work in areas with spotty internet connections. Job sites aren't exactly known for their

  WiFi coverage, and waiting for data to load from a server while standing in the mud isn't anyone's idea of productivity. That's where RxDB came into play.

  RxDB is essentially a local database that lives in the browser. Think of it as having a mini database server running right on your device, except it's way

  cooler because it automatically syncs with our main servers when you're back online. The "reactive" part means that when data changes, everything using that

  data updates automatically. No refresh buttons, no manual syncing – it just works.

  The way we've set it up starts with a central database instance that gets created when you first load the app. Here's what that looks like in our

  rxdb-instance.ts:

  export function getRxdbInstance(): Promise<RxDatabase<Collections>> {

    if (!dbInstance) {

      dbInstance = createRxDatabase<Collections>({

        name: 'ace-db',

        storage: validateAjvStorage,

      }).then((db) => {

        return db.addCollections({

          projects: { schema: projectSchema },

          bids: { schema: bidSchema },

          // ... other collections

        });

      });

    }

    return dbInstance;

  }

 


We've carefully designed this to be a singleton – you only get one database instance no matter how many times you call for it. This prevents the chaos of

  having multiple databases trying to sync with each other.

  Each type of data gets its own collection with a strict schema. Take our bid schema for example – it defines exactly what a bid looks like:

  properties: {

    _id: { type: 'string', maxLength: 1024 },

    name: { type: 'string', maxLength: 255 },

    project_id: { type: 'string', maxLength: 1024 },

    bidding_trades: {

      type: 'array',

      items: {

        oneOf: [

          { type: 'string' },

          { type: 'object' }

        ]

      }

    },

    // ... more fields

  }

 


Notice how we handle bidding_trades? It can be either strings or objects because our API sometimes returns just IDs and sometimes full objects. This

  flexibility prevents validation errors while maintaining data integrity.

  The real beauty comes from our layered architecture. We built a BaseRxdbStorage class that encapsulates all the complex RxDB operations:

  export abstract class BaseRxdbStorage<T> {

    get$(query: Partial<T> = {}): Observable<T[]> {

      if (!this.collection) {

        return from(Promise.resolve([]));

      }

      return from(this.collection.find(query).exec()).pipe(

        map((docs) => docs.map((doc) => doc.toJSON(false) as T))

      );

    }

    upsert(item: T): Observable<void> {

      const cleaned = ensureTimestamps(stripV(item));

      return from(this.collection.upsert(cleaned)).pipe(map(() => void 0));

    }

  }

  Each method does one thing and does it well. The get$ method queries data, upsert saves or updates. Notice the stripV function? That removes version fields

  that our backend adds but RxDB doesn't need. Small details like this keep the system running smoothly.

  Our domain-specific storage services inherit from this base, adding their own special sauce:

  export class BidStorageService extends BaseRxdbStorage<BidInterface> {

    constructor(logger: LoggerService) {

      super(logger);

      getRxdbInstance().then((db) => {

        this.collection = db.bids;

        this.collection.preInsert((doc) => {

          doc.createdAt = new Date().toISOString();

          doc.updatedAt = new Date().toISOString();

        }, true);

      });

    }

  }

  The abstraction continues up through our share services and facades. By the time a component needs data, it's as simple as:

  // In component

  bid = this.bidFacade.selectedBid;

  // In template

  @if (bid()) {

    <h1>{{ bid().name }}</h1>

  }

  The component doesn't know or care whether that bid came from RxDB, the API, or was calculated from other data. It just works.

  Now, about RxDB Premium – we're currently using the open-source version, which has been fantastic for getting us started. As ACE grows and we add more

  features, upgrading to Premium becomes less of a luxury and more of a strategic necessity. The collection limit in the open-source version means we've had to

  make hard choices about which features get offline support. With Premium, we could enable offline functionality for job costing, equipment tracking, and the

  dozen other features our users are requesting.

  The investment in Premium isn't just about removing limits – it's about ensuring our field crews never lose work due to connectivity issues. When you're

  managing million-dollar construction projects, the cost of Premium is negligible compared to the value of having reliable, always-available data. Plus, the

  advanced conflict resolution and encryption features in Premium would let us handle sensitive bid data with even more confidence.

  This architecture has fundamentally changed how our users work. They trust the app because it's always there for them, whether they're in a basement reviewing

   plans or on a roof updating schedules. That's the power of thoughtful offline-first design with RxDB.


 
 
 

Comments


bottom of page