Apr 22nd, 2024
The social app experiment,
Social Beaver
, goes offline after more than three years of continuous and uninterrupted use. What started as a mini project to master the React state management container Redux ended in a full-fledged Twitter-like social network running on Firebase.
The scope of this post is neither educational nor to give a detailed code overview and explain all the functionalities and rationale behind my code implementation.
No, I will keep it short and simple. Everybody today knows how social networks work and what you can expect by using one. There is no need for me to go into the details. The post is mostly for me to remember the platform and for anyone reading to see what I was doing in my first years of coding.
In the following paragraphs, I will briefly mention a few of the main platform's functionalities along with their screenshots.
If you, for some reason, decide to stick to the end, you're in for a treat. I will showcase how a simple comment to a post triggered a bunch of things in the background. A trivial explanation for seasoned coders, but it might be interesting for all the rest.
First and foremost, I must mention that the platform was coded mobile-first. I wanted it to be responsive. I believe more people nowadays use social networks on their mobile devices than on their stationary computers.
It is not a social network without users, and you can't have users without them creating an account and being able to sign in with it.
A call to Firebase's auth service on the backend, and poof, the user is logged in. I'm not a fan of Firebase anymore, but they sure made it simple for developers.
Fully responsive app - authentication page
Users could access the homepage without being logged in, but, they had to be logged in to interact with the platform. This was made both for the safety of users and my wallet.
Data tied to accounts could be quickly moderated, and quick moderation prevented me headaches if Social Beaver would be a target of a content spamming attack. Storage and requests get expensive quickly on Firebase. I have indeed set the upper monthly cap on my spending there, but I prefer to avoid hitting it if possible.
Homepage desktop
Homepage mobile
Users could change their photo, bio, location and website link. This is just a fraction of what users should be able to do with their profiles, as they had no way to change their passwords, request their user data, or set up additional security for authentication like 2FA and anti-phishing phrases, etc.
But as this was a learn-along project, without me having any intention to push it to the masses, you can understand why the above features are lacking.
Profile editor
It wouldn't be called a social network if users couldn't create posts, read posts and express their opinions in the form of comments or likes. There really is nothing innovative about these features, but they needed to be mentioned for the above reasons.
Add comment
You've made it to the end of the article. Thank you! As promised, I'll show you what a comment to a post triggers in the background.
The above photo shows you the user interface for adding a comment. When you press the submit button, the frontend dispatches a POST request to the backend.
export const submitComment = (biteId: string, commentData: string) => (dispatch: Dispatch) => {
axios.post(`/bites/${biteId}/comment`, commentData)
.then((res) => {
dispatch({
type: SUBMIT_COMMENT,
payload: res.data,
});
dispatch<any>(clearErrors())
})
.catch((err) => {
dispatch({
type: SET_ERRORS,
payload: err.response.data,
});
});
}
Submit a comment on UI
Using Express.js, the backend listens to the route's POST events.
app.post('/bites/:biteId/comment', FBAuth, commentBite)
Add comment endpoint
When the event is received on the monitored route, the
FBAuth
middleware first ensures that the user is authenticated and can dispatch such events.
export const FBAuth = (req: Request, res: Response, next: NextFunction) => {
let idToken;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
idToken = req.headers.authorization.split('Bearer ')[1];
} else {
// 403 - unauthorized
console.error('No token found');
return res.status(403).json({ error: 'Unauthorized' }).end();
}
// verify token
admin
.auth()
.verifyIdToken(idToken)
.then((decodedToken) => {
req.user = decodedToken;
return db
.collection('users')
.where('userId', '==', req.user.uid)
.limit(1)
.get()
})
.then((data) => {
req.user.handle = data.docs[0].data().handle;
req.user.imageUrl = data.docs[0].data().imageUrl;
return next();
})
.catch((err) => {
console.error('Error while verifying token ', err);
return res.status(403).json(err);
})
}
Authentication middleware
If the authentication is successful, the FBAuth middleware adds the user's handle and profile image URL to the request and forwards it to the function that updates the Firebase's database (Firestore) with the just added comment.
export const commentBite = (req: Request, res: Response) => {
if (req.body.body.trim() === "") {
return res.status(400).json({ comment: "Must not be empty"}).end();
}
const newComment: any = {
body: req.body.body,
userHandle: req.user?.handle,
createdAt: new Date().toISOString(),
biteId: req.params.biteId,
userImage: req.user.imageUrl
};
db
.doc(`bites/${newComment.biteId}`)
.get()
.then((doc) => {
if (!doc.exists) {
res.status(404).json({ error: "Bite not found" }).end();
}
return doc.ref.update({ commentCount: doc.data()!.commentCount + 1 })
.then(() => {
return db
.collection('comments')
.add(newComment)
})
})
.then(() => {
return res.json(newComment);
})
.catch((err) => {
console.error(err);
return res.status(500).json({ error: "Something went wrong" });
});
}
Adding comment to the database
Finally, a listener is triggered with a newly added comment, which, in response, sends a notification for the received comment to the user whose post received it.
export const createNotificationOnComment = functions
.region('europe-west3')
.firestore
.document('comments/{id}')
.onCreate((snapshot) => {
return db
.doc(`/bites/${snapshot.data().biteId}`)
.get()
.then((doc) => {
if (doc.exists && doc.data()!.userHandle !== snapshot.data().userHandle) {
return db
.collection('notifications')
.doc(snapshot.id)
.set({
createdAt: new Date().toISOString(),
recipient: doc.data()!.userHandle,
sender: snapshot.data().userHandle,
type: "comment",
read: false,
biteId: doc.id
})
}
return;
})
.catch((err) => {
console.error(err);
})
});
Adding notification
At this point, a WebSocket could push a real-time notification update to the user as he receives the comment and notification, but for simplicity's sake, I opted out of it.
New comment notification
Clicking on the notification shows the comment
The above is just one of the examples. Many listeners are firing in the background. Just to name a few examples: