You have some new innovative, fun idea of social media.You want a secure, smart, fast and scalable app.This is how you can achieve this on limited time and budget.
This article based on personal experience and hours of research. It will mostly contain code examples.
Ingredients:Node.jsGCP — scalable servers and extra toolsFirebase — real time databaseCloudflare — security and performanceMailgun — emails
We will use one App engine and split it into 3 microservices1st microservice — Backend server2nd microservice — Cron server3rd microservice — Web server
I will use React for the client side.The Backend solutions are good for other client options.
Before you write the first line of code
You need to set your goals for the first release.It’s important to decide what is necessary for the first version and what you can add after.
Choose a name for your social media app.Check if the domain is available.You can use Google domains or GoDaddy for this.
Open accounts and create new project at GCP, Firebase Cloudflare and Mailgun.
Open developers account on Facebook and Instagram to enable login options. (Make sure you are sending a review request, it might take some time).
Client side friendlySecurityAuto scalingEasy to maintainKeep logsReal time reports on bugs
Yes, you can achieve those goals with a traditional RESTful API server, but, I would like to show you another alternative which will save you a lot of time.
We are going to use Firebase queue, Firebase real-time database and Google App engine to create that.
This will be my folders hierarchy.
📁config📁keys➥serviceAccountKey.jsonconfiguration.jsonindex.js
📁services📁firebase➥index.js📁image_manipulation➥index.js📁notifications➥index.js
📁utils➥index.js
app.jsapp.yamlpackage.json
Node dependencies:
"fcm-node": "^1.0.15","firebase": "^5.4.0","firebase-admin": "^6.0.0","firebase-queue": "^1.6.1","mailgun-js": "^0.20.0"
Image manipulation dependencies:
"@google-cloud/storage": "^1.5.1","@google-cloud/vision": "^0.14.0","image-size": "^0.6.2","imagemin": "^5.3.1","imagemin-giflossy": "^5.1.10","imagemin-jpeg-recompress": "^5.1.0","sharp": "^0.18.4","smartcrop": "^1.1.1","smartcrop-sharp": "^1.0.5"
Let's start with the config/configuration.json:
{"firebase": {"projectId": "YOUT_PROJECT_ID","url": "YOUT_PROJECT_DB_URL","serviceAccount": "config/keys/serviceAccountKey.json"},"mailgun": {"apiKey": "YOUT_MAILGUN_KEY","domain": "YOUT_MAILGUN_DOMAIN"},"app": {
config/index.js (note that there is configuration.dev.json)
const config = process.env.NODE_ENV === 'production'? require('config/configuration.json'): require('config/configuration.dev.json');
module.exports = config;
app.yaml
service: apiruntime: nodejsenv: flexenv_variables:NODE_ENV: prod
services/firebase/index.js
const admin = require('firebase-admin');const config = require('config');
admin.initializeApp({credential: admin.credential.cert(require(config.firebase.serviceAccount)),databaseURL: config.firebase.url});
const db = admin.firestore();const tasksDB = admin.database();const tasksQueue = tasksDB.ref('tasks_queue/');
db.settings({ timestampsInSnapshots: true });
// Here we will write our db functions
module.exports = {tasksQueue
Now we can start writing our server (app.js)
Personally, I like to set my require() paths like this: (you can read more about it here)
process.env.NODE_PATH = __dirname;require('module').Module._initPaths();
We will start with the setup and add three simple ‘tasks’ saveUser , updateUser and likePost.
addLogs function will save the incoming task in our log table at Firebase.
mailError will email the task data with the error message in case of an error.
And processTask will listen to the tasks_queue table on Firebases real time DB and whenever a new task written in that table it will process the task according to its type. If there are more tasks than he can handle App engine will autoscale.
As you can see we need to add some function to our Firebase endpoint file
function setData(ref, data){if (typeof ref === 'string') return db.doc(ref).set(data);return ref.set(data);}
function updateData(ref, data){if (typeof ref === 'string') return db.doc(ref).update(data);return ref.update(data)
ateLogs(date, taskType, time, task) {tasksDB.ref(`logs/${date}/${taskType}/${time}/task`).set(task)
ion likePost(postId){return new Promise((resolve, reject) => {const postRef = db.collection('posts').doc(postId);return db.runTransaction((transaction) => {return transaction.get(postRef).then((postDoc) => {if (!postDoc.exists) resolve();const newLikes = postDoc.data().likes + 1;transaction.update(postRef, { likes: newLikes });});}).then(resolve).catch(reject);});}
// Don't forget to add them to the module exports
This is how the tasks table will look like:
Another thing we may want to add to our server is image manipulation. Let's say, a user uploaded a photo to our fun social media, We will need to compress that image, Maybe alert us if it is offensive and we may even want to create a smart thumbnail for it.
We will add the task to handle that request
Now, lets have a look at services/image_manipulation/index.js (I will not get into details on this one)
Our most valuable retention tool.
We need to inform the users if, for example, someone like their post. We want to be able to send messages and notifications to users across platforms, Android, iOS, and the web.
A good thing will be also to save it in our DB so we can display all of the notification inside the app.
Let’s change our like_post function
function likePost(data = {}) {return new Promise((resolve, reject) => {const { user, postId, postOwner, } = data;Promise.all([db.likePost(postId),db.newNotification('like', user, postOwner),]).then(resolve).catch(reject);});}
And add newNotification to our Firebase endpoint file
const fcm = require('services/notifications');const notificationsText = require('texts/en/notifications');
function newNotification(tag, from, to) {return new Promise((resolve, reject) => {const notification = notificationsText(tag, from, to);if (to.deviceToken) fcm.sendNativeNotification(notification, deviceToken);const newNotificationRef = db.collection(`messages/${to.uid}/notification`).doc();newNotificationRef.set(notification).then(resolve).catch(reject);});}
services/notifications/index.js
So there you have it, API server that does exactly what you need to bootstrap (Well, you will still need to write functions that will handle your client tasks).
Security rules can be easily set through the rules tab on the Firebase console. You can read more about it here.
If we need some schedule functions to run in the background. This is how we will achieve that:
In this example our schedule function will like a random post
The folder hierarchy
📁bin➥www
📁config📁keys➥serviceAccountKey.jsonconfiguration.jsonindex.
📁cron_jobs➥ index.jsrandom_like.js
📁services📁firebase➥index.js
📁utils➥index.js
app.jsapp.yamlcron.yamlpackage.json
Node dependencies:
"express": "^4.16.3","firebase": "^5.4.0","firebase-admin": "^6.0.0"
The configurations remain the same as the API server.
app.yaml
service: cronruntime: nodejsenv: flexenv_variables:NODE_ENV: production
cron.yaml
cron:- description: Like a Random Posturl: /like_random_postschedule: every 10 minutestimezone: Etc/GMTtarget: cron
app.js
process.env.NODE_PATH = __dirname;require('module').Module._initPaths();
const express = require('express');const app = express();app.enable('trust proxy');
app.use('/', require('cron_jobs'));
app.use((err, req, res, next) => res.status(500).send(err.message || 'Something broke!'));
module.exports = app;
cron_jobs/inde
t express = require('express');const randomLike = require('cron_jobs/random_like');const router = express.Router();
// [START routing]router.get('/like_random_post', randomLike);// [END routing]
module.exports = router;
cron_jobs/random_like.js
// At the Firebase endpoint file
function addTask(type, data){return tasksDB.ref('tasks_queue/tasks').push({ type, data });}
As you can see at the cron.yaml file, a GET request will be fired every 10 minutes to the /like_random_post endpoint (target: cron microservice). The function gets a random post from the DB and add a ‘like_post’ task to our tasks queue.
That’s conclude the first part of our story, by now you should have a working server that can be use for any type of app, native or web.
This server will serve WebApp pages to the users. Basically, we will need only one route since we will handle all the routing with React-router. However, when our users will share their posts to others social media, we will want it to be attractive. Rich previews are a great tool to achieve that.
In order to support Rich preview we will need to adjust our html file for every post.
I will add an example code here of how it can be done easily, this is just an example there are others ways to do it.
Folder hierarchy:
📁bin➥www
📁config📁keys➥serviceAccountKey.jsonconfiguration.jsonindex.
📁routes➥ index.jspost.js
📁services📁firebase➥index.js
📁utils➥index.js
📁views➥index.js➥html_template.js
app.jsapp.yamlpackage.json
app.yaml
runtime: nodejsenv: flexenv_variables:NODE_ENV: production
app.js
As you can see, we will server our files zipped for better performance.
routes/index.js
'use strict';
const express = require('express');const router = express.Router();const htmlTemplate = require('../views');
// [START post]router.get('/post/*', require('./post'));// [END post]
// [START app]router.get('/*', (req, res) => {// console.log(req);return res.send(htmlTemplate())});// [END app]
module.exports = router;
routes/post.js
html_template.js
There are plenty of great ‘how to build a React web app’ articles out there. Here I will focus on some social media web app challenges.
FeedLive ListenersImages uploadsRich previews
Let’s start with the root of our WebApp
pushNotification is our the way to communicate with the users of the app, which looks something like this:
For that we will use ‘toastr js’.
import toastr from 'toastr';
export default function pushNotification(message, type = 'error') {toastr.options.positionClass = 'toast-bottom-full-width';toastr.options.showMethod = 'slideDown';toastr.options.hideMethod = 'slideUp';toastr.options.hideDuration = 300;toastr.options.newestOnTop = false;toastr.remove();switch (type) {case 'info':toastr.info(message);break;case 'error':toastr.error(message);break;case 'success':toastr.success(message);break;case 'warning':toastr.warning(message);break;default:toastr.error(message);break;}}
Firebase end-point file:
import firebase from 'firebase/app';import 'firebase/auth';import 'firebase/storage';import 'firebase/firestore';import 'firebase/database';import config from 'config';
class Firebase {
static serverTimestamp() {return firebase.firestore.FieldValue.serverTimestamp();}
constructor() {firebase.initializeApp({apiKey: config.firebase.api_key,authDomain: config.firebase.domain,projectId: config.firebase.project_id,storageBucket: config.firebase.bucket,});this.auth = firebase.auth();this.db = firebase.firestore();this.db.settings({ timestampsInSnapshots: true });this.tasksDB = firebase.database();this.tasks = this.tasksDB.ref('tasks_queue/tasks');this.storage = firebase.storage();}
}
export default Firebase;
📁feed📁view➥feed_item.js➥index.js➥index.js
Hopefully we will need to handle a lot of posts. In order to manage it correctly, we need:
1. Paginate data with query cursors
2. Highly efficient infinite scrollable container
3. On-view post updates listener
“Controller”:
For infinite scrollable container we will use ‘react-infinite’
“View”:
For the post item we will want to add some listeners. For example, likes — counter should be constantly updated in real time. However, setting listeners to the huge amount of posts can hit our webapp performance significantly, in order to make it efficient we will listen to changes only the posts that are in-view and use ‘react-visibility-sensor’ to do that.
Let’s add some functions to our Firebase end-point file
getRecentPosts(startAfter = null) {const postRef = startAfter? this.db.collection('posts').orderBy('created_at', 'desc').startAfter(startAfter.created_at).limit(config.app.feed_paging): this.db.collection('posts').orderBy('created_at', 'desc').limit(config.app.feed_paging);return postRef.get().then(snapshot => snapshot.docs.map(doc => doc.data()));}
postChangesListener(postId, callback) {return this.db.collection('posts').doc(postId).onSnapshot((doc) => {if (callback) callback(doc.data());});}
addTask(type, data) {return this.tasks.push({ type, ...data });}
We want our users to be able to upload photos to our web app, we want to create thumbnails for those photos, we don’t want them to wait a long time for it to finish. In order to accomplish that we will need to lower the photo size (for faster upload and to create the thumbnail).
UploadPhoto Component:
When we resize a photo we need to take its exif data into account, otherwise we may encounter orientation issues.
ImageTools:
Note that the Firestore data manipulation doesn’t happen on the client side, but instead we add tasks to our server through Firebase RD. This is only for security reasons.
I am using webpack 4 and this is my webpack.config.js, if you need code splitting and gzip you can use it as an example.
After you finish writing your first version and deploy everything to GCP you can now add your App Engine records to your Cloudflare DNS settings and get a free performance and security boost including SSL, CDN and much more.
Here’s my final tip for building your new fun social media: include analytics for everything. That is how you would know what your users are doing. It will help you improve and invest your time on the things that matter to them.
I would love to get your comments and suggestions. If you have any questions, feel free to contact me at my linkedin. Thank you for reading! Spread the love :)