For this guide, we will be using to go through various concepts of real-time CRUD. For part 1, we will only consider the front end of our application. https://github.com/SocketCluster/sc-crud-sample sc-crud-sample To implement real-time , we need the following things: CRUD A database (in this case ). RethinkDB A WebSocket server + client framework which supports pub/sub ( ). SocketCluster A back end module to process CRUD requests and publish change notifications ( ). sc-crud-rethink A front end model component to represent a single resource from the database ( ). SCModel A front end collection component to represent a collection of resources from the database ( ). SCCollection A reactive front end framework to render the data from model and collection components ( ) VueJS For the app, one of the goals was to keep the front end as simple as possible so that it could work without bundling tools or compile steps. You may still want to bundle all your front end scripts together when serving the app in production (e.g. to speed up loading and to support old browsers) but not having to depend on a bundler (e.g. ) during development means that you can iterate more quickly when coding your front end. Also, if you want to switch to HTTP2 now or in the future (e.g. to push assets to clients without latency), then you may decide that you don’t need a bundler altogether. sc-crud-sample WebPack The main looks like this: **index.html** ^ Here we’re just importing a CSS stylesheet and defining some global styles inline. If our app was bigger, we’d probably want to keep all our CSS style definitions in separate files but for now we chose to keep it simple. In the above definition, we have a tag — Vue will replace this tag with our component; this component is defined inside our script which we import via the tag. The component is the entry point of our entire VueJS front end logic. Note that we use the ES6 modules syntax to import dependencies within our code. <div id="app"></div> app-inventory <script src="/app-inventory.js" type="module"></script> app-inventory Note that for our application scripts, we try to use paths relative to the root directory; this makes it easier to find things when reading through various parts of the code. Our module looks like this: **app-inventory.js** ^ This file contains all the logic required to bootstrap our front end application. At the top, we’re importing some scripts/functions which will allow us to generate all the different pages within our app. let socket = window.socket = socketCluster.create(); ^ This creates a WebSocket-based socket object which our and components will use to load data from and send data to our server and through which those components will receive change notifications when data changes on the server side. socketcluster-client SCModel SCCollection let pageOptions = {socket}; let PageCategoryList = getCategoryListPageComponent(pageOptions);let PageCategoryDetails = getCategoryDetailsPageComponent(pageOptions);let PageProductDetails = getProductDetailsPageComponent(pageOptions);let PageLogin = getLoginPageComponent(pageOptions); ^ Here we’re constructing all the VueJS page components which are used in our app. We’re passing a reference to our socket to every page component so that they can bind their own and components to it. We share a single global socket object with all our components in the same way that we would share a single global store between all components if we were following the architecture — In our case, this approach allows multiple components to share the same pub/sub channels to consume real-time data from the server. SCModel SCCollection Redux let routes = [{path: '/category/:categoryId/product/:productId',component: PageProductDetails,props: true},{path: '/category/:categoryId',component: PageCategoryDetails,props: true},{path: '/',component: PageCategoryList,props: true}]; let router = new VueRouter({routes}); ^ We define all the routes in our app and link them to the relevant page components; we pass everything to and let it do its magic. VueRouter Everything is pretty standard VueJS boilerplate. The most interesting part is at the bottom of the script: template: `<div><div v-if="isAuthenticated" style="padding: 10px;"><router-view></router-view></div><div v-if="!isAuthenticated" style="padding: 10px;"><page-login></page-login></div></div>` ^ This means that if is true, VueJS will render our router (which will render the relevant page based on the current URL path). If is false, VueJS will show the user with a login page from which they will be able to login. SocketCluster supports JWT authentication, so we will listen for an event on the socket to check when it becomes authenticated; we will update our Vue app component’s property to reflect the of our socket. This is the important part: isAuthenticated isAuthenticated authStateChange isAuthenticated authState created: function () {this.isAuthenticated = this.isSocketAuthenticated();socket.on('authStateChange', () => {this.isAuthenticated = this.isSocketAuthenticated();}); ... Then there is some ugly code just below it… ... this.\_localStorageAuthHandler = (change) => { // In case the user logged in from a different tab if (change.key === socket.options.authTokenName) { if (this.isAuthenticated) { if (!change.newValue) { socket.deauthenticate(); } } else if (change.newValue) { socket.authenticate(change.newValue); } } }; window.addEventListener( 'storage', this.\_localStorageAuthHandler ); ^ That’s just to make authentication work across multiple browser tabs; so in our case, if the user has our app open in multiple tabs and they log into (or log out of) either one of those tabs, then we want the authentication state of our app to update immediately to match across both open tabs. The module looks like this: **page-login.js** ^ The reason why we export a function is to allow us to receive the global object from our upstream app component. getPageComponent socket The most important code in this component is: socket.emit('login', details, (err, failure) => {if (err) {this.error = 'Failed to login due to error: ' + err;} else if (failure) {this.error = failure;} else {this.error = '';}}); ^ This will attempt to authenticate our client socket. The event will be handled by our custom server-side logic; it will check if the username and password combination is correct and, if so, our client socket will emit an event; if you recall our logic (and template) in , this event will cause VueJS to hide our login page and activate our Vue router component. login authStateChange app-inventory Once the router becomes activated, it will render one of our page components depending on our the current URL. All our pages are relatively similar in their basic structure so we will analyse the most advanced one which is the component in ; this page represents a detailed view of a which contains a bunch of resources. In our app, all resources are associated with a ; this allows us to filter items based on which group they belong to. **page-category-details.js** Category Product Product Category This is what the category details screen looks like (URL ): http://localhost:8000/#/category/:categoryId This page contains two tables which are lists of resources. The table at the top represents a list of all resources which belong to the current category (with and links to navigate through all products). The second table underneath only lists up to 5 products which belong to the current category AND which are about to run out of stock soon (have the lowest qty). Both of these tables update in real-time as new products are added and as the qty and price of the products change — The best way to test this is to open the app in multiple browser tabs and watch the changes instantly sync between them. Product Product Prev page Next page The first text input box on the page lets you add new products to the current category. The second text input box lets you change the threshold qty for the second table so that you can reduce it down to the items that have the smallest qty — This feature shows that you can generate complex real-time views based on multiple parameters. The code for the component looks like this: **page-category-details.js** At the very beginning, we import our and components. The next most interesting part in this code is this: SCCollection SCModel props: {categoryId: String}, ^ This comes from which reads it from the URL. If our URL looks like this: , then our will be the string . categoryId VueRouter http://localhost:8000/#/category/4dfbfe47-d2fe-4e12-9ba1-26c8877221e8 categoryId "4dfbfe47-d2fe-4e12-9ba1-26c8877221e8" The next most important part in our code is this: data: function () {this.categoryModel = new SCModel({socket: pageOptions.socket,type: 'Category',id: this.categoryId,fields: ['name', 'desc']}); ... ^ This is where some of the real-time updating magic happens — Here we’re creating a model instance; this front end object will use the object to essentially bind itself to the relevant resource on the server and will make sure that it always shows the latest data. The also exposes methods to update the underlying resource on the server side. Category socket SCModel The property here refers to the table name inside our RethinkDB database, the is the id of the resource in the database (RethinkDB uses ). The property is an array of fields that we want to include as part of this model — the underlying resource may have many more fields; so a bit like with GraphQL, we only specify the ones which we need for the current page. type id uuids fields The following code is where we instantiate our main which will provide data to the table at the top of the page: SCCollection this.productsCollection = new SCCollection({socket: pageOptions.socket,type: 'Product',fields: ['name', 'qty', 'price'],view: 'categoryView',viewParams: {category: this.categoryId},pageOffset: 0,pageSize: 5,getCount: true}); ^ Like in , the property refers to the table name in RethinkDB, also as before, the property is an array of fields that we want to include as part of this collection. SCModel type fields The property holds the name of the view which this collection represents; a view is just a transformation applied to a collection (e.g. a filtered/sorted subset). In our case, the represents a subset of the collection which only contains products whose field matches the current of our page. view categoryView Product category categoryId The option holds an object which contains parameters which we pass to our view generator on the back end; the name of the properties of the object must match a field name on the underlying resource (in this case the resource has a field). The is relatively simple because it only has a single property — That said, it’s possible to have collections without any ; it’s also possible to have a collection without views; in that case, the collection component will just bind to the entire database table without any filtering. viewParams viewParams Product category productsCollection viewParams viewParams The option refers to the offset index from which to start loading the collection (starting at the nth item). The defines the maximum number of items that the collection can contain at any given time. The property is a boolean which indicates whether or not we should load metadata about the total number of resources in the collection. pageOffset pageSize getCount The next collection which is declared in our component is the : lowStockProductsCollection this.lowStockProductsCollection = new SCCollection({socket: pageOptions.socket,type: 'Product',fields: ['name', 'qty', 'price'],view: 'lowStockView',viewParams: {category: this.categoryId, qty: lowStockThreshold},viewPrimaryKeys: ['category'],pageOffset: 0,pageSize: 5,getCount: false}); ^ This provides the data for the second table on the category details page. It looks very similar to the except for a few properties. The first difference is that it uses a different view — instead of ; also it has one additional : a property. The property allows us to adjust the threshold used to transform/filter our . SCCollection productsCollection lowStockView categoryView viewParams qty qty lowStockView Another major difference is that the defines a array which contains as the only primary key; this is required because real-time filtered views rely on the fact that some relationships between resources exist within the view (e.g. All products which belong to the same have the same value as their property). Since in this case we’re modeling an relationship (e.g. in this case, our view represents a relationship where is ), we cannot include it as part of our view’s primary key; we still get some performance benefits from having as our primary key (better than no primary key). lowStockView viewPrimaryKeys 'category' equality categoryView category inequality Product.qty less than lowStockThreshold category By default, the property is optional; if not provided, all the properties in will be used as primary keys — being explicit about the primary keys allows us to meet the equality requirement for optimum real-time updates on collections. If are specified for a view in your back end schema (in ), then you also specify matching for your collection on the front end. viewPrimaryKeys viewParams Important note: primaryKeys worker.js must viewPrimaryKeys Note that you can pass an empty array as ; this will guarantee that automatic real-time updates of the collection will work but the downside is that this approach will cause the collection to subscribe to every change notification on the entire table (instead of only a small affected subset as is the case for ) — the category primary key acts as a natural sharding mechanism; the more categories there are, the more efficiently change notifications will be delivered to users. viewPrimaryKeys categoryView For the back end guide, see: Real-time CRUD guide: Back end (part 2)