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.
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.
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
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
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
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">
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
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>
The command will create a starter Node.js project.
Secondly, we need to install the required dependency by running the command below:
cd functions/metric-stripe-function
npm i axios
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}¤cy=${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',
})
);
};
The snippet above does the following:
- Imports the required dependency
-
Lines 4-10 create an API body that consists of
amount
,currency
, andpayment_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
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.
We can also confirm the deployment by navigating to the Function tab on the Appwrite console.
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.
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
Setting up the project
First, we need to add Appwrite as a dependency by running the command below:
npm i appwrite && npm i
Then, we can run our project using the command below:
npm run dev
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 |
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.
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
);
};
The snippet above does the following:
- Imports the required dependency
- Initializes Appwrite
client
anddatabases
with required arguments - Creates
account
,getUserDetails
,createSubscription
, andupdateUserSubscription
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>
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>
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>
The snippet above does the following:
- Conditionally shows premium metrics using the user’s subscription status
- Updates the
Locked
component to receivename
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>
The snippet above does the following:
- Imports the required dependency
- Defines the expected component props
- Creates an
onSubmit
function that uses thecreateSubscription
andupdateUserSubscription
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
https://media.giphy.com/media/dGd4daA38V4GWjeRg2/giphy.gif
We can validate the subscription by checking the Appwrite function Executions tab and 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)
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?
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?