DEV Community

Cover image for Build a serverless subscription system with Stripe and Appwrite for premium user roles in Nuxt
Demola Malomo for Hackmamba

Posted on

Build a serverless subscription system with Stripe and Appwrite for premium user roles in Nuxt

Serverless functions, lambda functions, functions as a service (FAAS), and serverless frameworks, among others, are some innovations leveraging serverless architecture. Such an architecture allows developers to build robust applications with improved resilience, reduced cost, and unburdened with the overhead of maintaining servers.

In this post, we will learn how to build a metric tracker with premium features in a Nuxt.js application with Appwrite’s serverless function and Stripe.

GitHub Links

The project source codes are below:

Prerequisites

To fully grasp the concepts presented in this tutorial, the following are required:

  • Basic understanding of JavaScript and Vue.js
  • Docker installation
  • An Appwrite instance; check out this article on how to set up an instance or install with one-click on DigitalOcean or Gitpod
  • Appwrite CLI installed
  • A Stripe account (sign up for a trial account, it is completely free)

Set up a Stripe account to process payments

To get started, we need to log into our Stripe account to get our Secret Key. The Secret Key will come in handy for processing payments in our application. To do this, navigate to the Developers tab and copy the Secret Key.

Stripe overview

Integrate Appwrite function with Stripe

By default, Appwrite supports Node.js as a runtime for creating serverless functions. To get started, we need to log into our Appwrite console, click the Create project button, input metric-tracker as the name, and then click Create.

Create project

Initialize function directory
With our project created on Appwrite, we can now create a directory that our function will use to process payments using Stripe. To do this, we first need to navigate to the desired directory and run the command below:

mkdir metric-stripe-function && cd metric-stripe-function
Enter fullscreen mode Exit fullscreen mode

The command creates a project folder called metric-stripe-function and navigates into this folder.

Secondly, we need to log in to the Appwrite server using the command-line interface (CLI).

appwrite login
Enter fullscreen mode Exit fullscreen mode

We will be prompted to input an email and password, which need to be the credentials we used to sign up for the Appwrite console.

Lastly, we need to link our function directory to the Appwrite project created earlier by running the command below:

appwrite init project
Enter fullscreen mode Exit fullscreen mode

We will be prompted with some questions on how to set up the project, and we can answer as shown below:

How would you like to start? <select "Link this directory to an existing Appwrite project">
Choose your Appwrite project <select "metric-tracker">
Enter fullscreen mode Exit fullscreen mode

Create Appwrite function inside the project
With our project succesfully set up, we can now proceed to creating a function by running the command below:

appwrite init function
Enter fullscreen mode Exit fullscreen mode

We will also be prompted with some questions about how to set up our function; we can answer as shown below:

What would you like to name your function? <input "metric-stripe-function">
What ID would you like to have for your function? (unique()) <press enter>
What runtime would you like to use? <scroll to node-18.0 and press enter>
Enter fullscreen mode Exit fullscreen mode

The command will create a starter Node.js project.

Starter project

Secondly, we need to install the required dependency by running the command below:

cd functions/metric-stripe-function 
npm i axios
Enter fullscreen mode Exit fullscreen mode

Thirdly, we need to modify the index.js file inside the src folder as shown below:

const axios = require('axios');

module.exports = async function (req, res) {
    const data = {
        amount: 100,
        currency: 'usd',
        payment_method: 'pm_card_visa',
    };

    const body = `amount=${data.amount}&currency=${data.currency}&payment_method=${data.payment_method}`;

    await axios
        .post('https://api.stripe.com/v1/payment_intents', body, {
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            auth: {
                username: 'REPLACE WITH STRIPE SECRET KEY',
            },
        })
        .then((_) =>
            res.json({
                status: 200,
                message: 'Subscription processed successfully',
            })
        )
        .catch((_) =>
            res.json({
                status: 500,
                message: 'Error processing subscription',
            })
        );
};
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependency
  • Lines 4-10 create an API body that consists of amount, currency, and payment_method of type card and using a test card (pm_card_visa)
  • Lines 12-30 make an API call to Stripe by passing in the body, set the authentication using the Secret Key, and return the appropriate response

Note: We hardcoded the amount, currency, and payment method in this post. However, Stripe supports multiple payment methods and options we can adopt in a production environment.

Lastly, we must navigate to the project terminal and deploy our function.

cd ../..
appwrite deploy function
Enter fullscreen mode Exit fullscreen mode

We will also be prompted about the function we would like to deploy. Select the metric-stripe-function function by pressing the spacebar key to select and the enter key to confirm the selection.

Select function to deploy

Sample of a deployed function

We can also confirm the deployment by navigating to the Function tab on the Appwrite console.

Deployed function

Lastly, we must update the deployed function permission since we need to call it from our Nuxt application. To do so, navigate to the Settings tab, scroll to the Execute Access section, select Any and click Update.

Update permission

Create a Nuxt app and set up database

With our function deployed and ready to accept payment via Stripe, we can now set up a Nuxt project. To get started, we need to clone the project by navigating to the desired directory and running the command below:

git clone https://github.com/Mr-Malomz/metric-tracker.git && cd metric-tracker
Enter fullscreen mode Exit fullscreen mode

Setting up the project
First, we need to add Appwrite as a dependency by running the command below:

npm i appwrite && npm i
Enter fullscreen mode Exit fullscreen mode

Then, we can run our project using the command below:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Running app (home page)
Running app (metric page)

Create a database and add sample data
First, we need to create a database with the corresponding collection, attributes, document, and add sample data as shown below:

name isSubscribed
John Travolta false

Collection

Lastly, we need to update our collection permission to manage them accordingly. To do this, navigate to the Settings tab, scroll down to the Update Permissions section, select Any, mark accordingly, and Update.

Select Any and update

Building the metric tracker with subscription system

To get started, we first need to create a components/utils.js to abstract the application logic from the UI and add the snippet below:

import { Client, Databases, Account, Functions } from 'appwrite';

const PROJECT_ID = 'REPLACE WITH PROJECT ID';
const DATABASE_ID = 'REPLACE WITH DATABASE ID';
const COLLECTION_ID = 'REPLACE WITH COLLECTION ID';
const FUNCTION_ID = 'REPLACE WITH FUNCTION ID';
const DOCUMENT_ID = 'REPLACE WITH DOCUMENT ID';

const client = new Client();

const databases = new Databases(client);

client.setEndpoint('http://localhost/v1').setProject(PROJECT_ID);

export const account = new Account(client);

export const getUserDetails = () =>
    databases.getDocument(DATABASE_ID, COLLECTION_ID, DOCUMENT_ID);

export const createSubscription = () => {
    const functions = new Functions(client);
    return functions.createExecution(FUNCTION_ID);
};

export const updateUserSubscription = (name, isSubscribed) => {
    const data = {
        name,
        isSubscribed,
    };
    return databases.updateDocument(
        DATABASE_ID,
        COLLECTION_ID,
        DOCUMENT_ID,
        data
    );
};
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependency
  • Initializes Appwrite client and databases with required arguments
  • Creates account, getUserDetails, createSubscription, and updateUserSubscription functions for managing user sessions, obtaining user details, creating the subscription, and updating user subscription status

PS: We can get the required IDs on our Appwrite Console.

Secondly, we need to update the app.vue file to use the account helper function to check if a user has a valid session.

<template>
  <div class="min-h-screen bg-brand-bg lg:flex lg:justify-between">
    <Sidebar />
    <section class="w-full h-auto px-4 py-2 lg:px-8">
      <NuxtPage></NuxtPage>
    </section>
  </div>
</template>

<script setup>
import { account } from "./components/utils";

//check session
onMounted(async () => {
  account
    .get()
    .then()
    .catch((_) => account.createAnonymousSession());
});
</script>
Enter fullscreen mode Exit fullscreen mode

Thirdly, we need to update the metrics.vue file in the pages folder as shown below:

metrics.vue Logic

<script setup>
import { getUserDetails } from "../components/utils";

//state
const user = ref(null);

//get user details
onMounted(async () => {
  getUserDetails()
    .then((res) => {
      user.value = res;
    })
    .catch((_) => {
      alert("Error loading user details");
    });
});
</script>
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependency
  • Creates application state and retrieves user details upon page load using the getUserDetails helper function

metrics.vue UI

<template>
  <div class="mt-20">
    <section class="flex items-center mb-10">
      <h1 class="text-3xl font-bold inline-block mr-4">Metrics</h1>
      <p
        class="
          px-2
          text-sm
          bg-indigo-900 bg-opacity-30
          text-indigo-900
          rounded-full
        "
      >
        premium
      </p>
    </section>
    <section class="flex" v-if="user?.isSubscribed">
      <div
        class="
          border
          w-full
          flex
          justify-center
          flex-col
          items-center
          h-96
          mr-2
          rounded-md
        "
      >
        <p class="text-gray-500 mb-4">Daily insight</p>
        <h1 class="text-4xl font-bold">456367</h1>
      </div>
      <div
        class="
          border
          w-full
          flex
          justify-center
          flex-col
          items-center
          h-96
          mr-2
          rounded-md
        "
      >
        <p class="text-gray-500 mb-4">Total insight</p>
        <h1 class="text-4xl font-bold">10956367</h1>
      </div>
    </section>
    <Locked v-else :name="user?.name" />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Conditionally shows premium metrics using the user’s subscription status
  • Updates the Locked component to receive name props

Lastly, we need to update the Locked.vue file in the components folder as shown below:

<template>
  <div class="flex w-full flex-col items-center justify-center h-96">
    <p class="text-center justify-center mb-4">
      Oops! Looks like you don't have access to this page
    </p>
    <p class="text-center justify-center mb-4">
      Get full access for by subscribing
    </p>
    <button
      class="px-8 py-2 bg-indigo-800 text-white rounded hover:bg-indigo-700"
      @click="onSubmit"
    >
      Upgrade to paid
    </button>
  </div>
</template>

<script setup>
import {
  createSubscription,
  updateUserSubscription,
} from "../components/utils";
const props = defineProps(["name"]);

const onSubmit = () => {
  createSubscription()
    .then((_) => {
      updateUserSubscription(props.name, true)
        .then((_) => {
          alert("Subscription created successfully!");
          window.location.reload();
        })
        .catch((_) => {
          alert("Error subscribing user!");
        });
    })
    .catch((_) => {
      alert("Error subscribing user!");
    });
};
</script>
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependency
  • Defines the expected component props
  • Creates an onSubmit function that uses the createSubscription and updateUserSubscription helper functions to manage subscription
  • Updates the UI to use the onSubmit function

With that done, we can restart a development server using the command below:

npm run dev
Enter fullscreen mode Exit fullscreen mode

https://media.giphy.com/media/dGd4daA38V4GWjeRg2/giphy.gif

We can validate the subscription by checking the Appwrite function Executions tab and Stripe Log tab.

Appwrite Execution tab
Stripe Log tab

Conclusion

This post discussed how to build a metric tracker with protected premium features using Appwrite and Stripe. The demo is a base implementation demonstrating Appwrite's support for building serverless architecture. Appwrite gives developers the magic wand to build medium- to large-scale applications at a reduced cost and excellent developer experience.

These resources may also be helpful:

Top comments (2)

Collapse
 
sonicviz profile image
Paul Cohen • Edited

Hi,
I tried running this but found some issues.

It fails (see below) but still sets the subscribed to true and lets you through the paywall.

It fails on the function with:
ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and '/usr/local/server/src/function/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
at file:///usr/local/server/src/function/src/main.js:1:15
at ModuleJob.run (node:internal/modules/esm/module_job:198:25)
at async Promise.all (index 0)
at async ESMLoader.import (node:internal/modules/esm/loader:409:24)
at async importModuleDynamicallyWrapper (node:internal/vm/module:437:15)
at async execute (/usr/local/server/src/server.js:126:32)
at async /usr/local/server/src/server.js:158:13

If you rename it as a cjs it then fails as it can't find main.js

Are you missing a common.js declaration in the package file or something?

Collapse
 
malomz profile image
Demola Malomo

Apologies for the delayed response. I suspect the issue may be due to a difference in the versions. Could you please check the repository above and ensure that our versions match?