

This article based on personal experience and hours of research. It will mostly contain code examples.
Ingredients:
Node.js
GCPโโโscalable servers and extra tools
Firebaseโโโreal time database
Cloudflareโโโsecurity and performance
Mailgunโโโemails
We will use one App engine and split it into 3 microservices
1st microserviceโโโBackend server
2nd microserviceโโโCron server
3rd 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 friendly
Security
Auto scaling
Easy to maintain
Keep logs
Real 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.json
configuration.json
index.js
๐services
๐firebase
โฅindex.js
๐image_manipulation
โฅindex.js
๐notifications
โฅindex.js
๐utils
โฅindex.js
app.js
app.yaml
package.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: api
runtime: nodejs
env: flex
env_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.json
configuration.json
index.
๐cron_jobs
โฅ index.js
random_like.js
๐services
๐firebase
โฅindex.js
๐utils
โฅindex.js
app.js
app.yaml
cron.yaml
package.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: cron
runtime: nodejs
env: flex
env_variables:
NODE_ENV: production
cron.yaml
cron:
- description: Like a Random Post
url: /like_random_post
schedule: every 10 minutes
timezone: Etc/GMT
target: 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.json
configuration.json
index.
๐routes
โฅ index.js
post.js
๐services
๐firebase
โฅindex.js
๐utils
โฅindex.js
๐views
โฅindex.js
โฅhtml_template.js
app.js
app.yaml
package.json
app.yaml
runtime: nodejs
env: flex
env_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.
Feed
Live Listeners
Images uploads
Rich 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ย :)
Create your free account to unlock your custom reading experience.