Before you go, check out these stories!

0
Hackernoon logoHow to build a fun social media on limited time and budget by@sagimedina

How to build a fun social media on limited time and budget

Author profile picture

@sagimedinaSagi Medina

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.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).

Backend API server

Goals:

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)

Notifications

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.

Cron server

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.

Part 2 — Frontend

Web server

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

Web App

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.

Goals:

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

📁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 });
}

Image upload

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:

Login example

Final Firebase end-point file

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 :)

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.