Crowdlinks project
Crowdlinks PMF Prototype
Work

Crowdlinks PMF Prototype

Oct 2019
Table of Contents

MVP First Phase

We built the MVP for the new business “CrowdLinks” using the following architecture and conducted PSF (Problem-Solution Fit) validation.

Some features were intentionally left unimplemented and operated manually to deliver the overall user experience—an example of a so-called “Wizard-of-Oz” style MVP.

TechnologyCategoryDescription
Nuxt.jsFrontendSPA Ă— SSR
TypeScriptFrontendEnables safer development through a strong type system
Firebase HostingInfrastructureHosts Nuxt as an SPA
Firebase Functions (SSR)InfrastructureExecuted via Firebase Hosting’s rewrite feature. Serves Nuxt on an Express server to enable SSR.
Firebase Functions (API)BackendServerless functions for the API. Triggered by requests to designated Hosting paths via rewrite rules.
Firebase AuthIDaaSProvides user authentication for the application.
CircleCICI/CDAutomates build, test, and deployment workflows.
yarnCI/CDPackage manager faster than npm
Atomic DesignDesign PatternUsed to design modular, reusable UI components.

MVP Post Problem-Solution Fit Phase

proto-architecture

Tech Stack

Main Application

Nuxt.jsNuxt application for the main product
TypeScriptEnables safer development through a strong type system
Firebase HostingCDN used to deliver the main Nuxt application as an SPA
Firebase Functions (SSR)Served via Hosting rewrite; distributes the main Nuxt app on an Express server for SSR
Firebase Functions (API)Serverless functions for the API; triggered by requests routed through Hosting rewrite rules
Firebase Function (ProxyServer)Requirement: deliver a different Firebase project under a subdirectory instead of a subdomain. Requests under /articles are proxied to the media Firebase project.
Firebase AuthProvides user authentication for the application.
Presentational and Container Component PatternComponent design pattern that separates responsibilities into “state” and “design”

Media Application

Nuxt.jsNuxt application for media content. Generated as a static site. Separated from the main app at the domain level.
TypeScriptEnables safer development through a strong type system
Firebase HostingHosts and serves the static media Nuxt application as a CDN
ContentfulHeadless CMS for articles. Changes trigger a webhook → relevant deployment flows start in CircleCI.

Build & Development Tools

CircleCIBecause of monorepo architecture, deploy workflows detect changed directories and only run the necessary deployments.
yarnPackage manager faster than npm

Core Technical Components / Decision Making

Monorepo

We chose a monorepo structure based on the team’s situation. The main goal was improving developer experience (DX) for better productivity.

Given our business status and a small team (only 3 engineers), it was inefficient for each person to separately handle frontend, backend, and infrastructure tasks. We needed everyone to cover all areas, but a multi-repo setup would require constant repository switching during development and reviews.

We judged that this cross-repo overhead was non-essential and wasteful. Thus, we adopted a monorepo to eliminate this switching cost.

Pros 👍

Cons 👎

Adding Media Application as a Microfrontend

We integrated a media-oriented Nuxt application within our monorepo as a microfrontend. This approach allowed us to:

TopicBenefit
Maintain unified codebaseWhile separating concerns between main application and media content
Choose subdirectory distribution(/articles) over subdomain for better SEO and user experience
Isolate infrastructureBy deploying the media app to its own Firebase project while maintaining seamless integration
Enable independent scalingOf media content without affecting core application performance

BLoC Pattern: Separating State and Presentation

After finding Atomic Design led to components with mixed responsibilities, we adopted the BLoC (Business Logic Component) pattern to create clearer separation of concerns:

This approach eliminated ambiguity in component responsibilities and improved maintainability.

proto-architecture

RBAC Implementation for Permission Management

As user roles became clearer, we implemented Role-Based Access Control (RBAC) to simplify authorization logic and eliminate code duplication.

Approach: Tell, Don’t Ask

Before: Permission checks were scattered throughout components, with each fetching data and implementing logic (Ask pattern).

After: Centralized permission checking through a dedicated function (Tell, Don’t Ask pattern).

Tell, Don't Ask

Reference: https://martinfowler.com/bliki/TellDontAsk.html

Example: Project Apply Button

Before - Logic embedded in component:

<button v-if="isAppliable" @click="apply">
    Apply for Project
</button>
</template>

<script lang="ts">
  computed: {
    currentUser(): CurrentUser | null {
      return this.$store.getters[CURRENT_USER_GETTER]
    },
    currentUserProfile(): UserProfile | null {
      return this.$store.getters[CURRENT_USER_PROFILE_GETTER]
    },
    isAppliable(): boolean {
      return (
        this.currentUser !== null &&
        this.currentUserProfile !== null &&
        this.currentUserProfile.hasEnoughProfile()
      )
    }
  }
</script>

After - Delegated to permission checker:

<button v-if="isAppliable" @click="apply">
    Apply for Project
</button>
</template>

<script lang="ts">
import { checkPermission } from "~/supports/auth/permissionChecker"
import { ParticularWriteAcl } from "~/config/acl/accessControlList"

  computed: {
    isAppliable(): boolean {
      return checkPermission(this.$store, ParticularWriteAcl.PROJECT_APPLY)
    }
  }
</script>

This abstraction centralizes permission logic, making it easier to maintain and modify access rules without touching component code.

Related Projects

    Mike 3.0

    Send a message to start the chat!

    You can ask the bot anything about me and it will help to find the relevant information!

    Try asking: