Building a Virtual Conference Ticket with Begin, AWS & Puppeteer


This year, inspired by the folks at Next.js Conf, I decided to build virtual tickets for my conference CascadiaJS. It's a fun feature for attendees because they get to help spread the word about an event they're excited about.

Here is the user flow for attendees choosing to create a virtual ticket:

  1. They click a link to create the virtual ticket.
  2. This sends them to Github for an OAuth flow.
  3. On success, Github returns with OAuth code.
  4. Web app stores success marker in DB for this attendee.
  5. Web app fires event to generate the virtual ticket image.

Let's walk through each of these steps!

0. Using Begin to build on AWS

The CascadiaJS 2021 web app is built on a framework called Arc and hosted on AWS via a new platform called Begin. The combination of Arc and Begin make it easy to build a full-stack web application that takes full advantage of AWS services like Lambda, DynamoDB and SNS without 99% of the boilerplate.

1. Creating the Github OAuth link.

First, you'll need to go to Github and create an OAuth application. Once you do this, you'll be provided with a Client ID and you can create a Client Secret. Store both of these values in your environment variables.

Building the link to kick-off a Github OAuth flow is as simple as constructing the URL below with your Client ID:

<a href="https://github.com/login/oauth/authorize?client_id=${ clientID }">Get Added to Directory</a>

2. This sends them to Github for an OAuth flow.

When the user lands on this page, they'll see the name of your Github OAuth app and the logo you've uploaded. Make sure these are things that folks will trust.

3. On success, Github returns with OAuth code.

Once the user authorizes this connection, Github will redirect to the Authorization callback URL that you will have configured and will pass along a code as a query parameter.

4. Web app stores success marker in DB for this attendee.

In the HTTP function that handles the OAuth callback, we use the code passed in the request to retrieve the public information for this Github user. We then update the attendees ticket record to store their Github username and avatar:

let info = await github(req)
await data.set({ table: 'tickets', ...ticket, github: info.login, avatar: info.avatar })

5. Web app fires event to generate the virtual ticket image.

Finally, the stuff you've really been waiting for: generating dynamic images!

First, since this image generation process can take time, I chose to kick-off an asynchronous job using Arc events which are sugar for more easily using AWS SNS. This way the HTTP functions returns to the user immediately, while imagine generation happens in the background.

const name = 'ticket-shared'
const payload = { number: ticket.number }
await arc.events.publish({ name, payload })

The event function, when invoked, is provided with the unique ticket number for this attendee. It uses this number to generate the image of virtual ticket:

let file = await screenshot({ number })

Let's dig into the screenshot module, since that's where the magic happens:

const chromium = require('chrome-aws-lambda')
require('puppeteer-core') function getBaseUrl() { let url if (process.env.NODE_ENV === 'testing') { url = 'http://localhost:3333' } else { url = `https://${ process.env.NODE_ENV === 'staging' ? 'staging.' : '' }2021.cascadiajs.com` } return url
} module.exports = async function screencap({ number }) { let browser let baseUrl = getBaseUrl() // set-up headless browser let height = 628 let width = 1200 let deviceScaleFactor = 1 try { browser = await chromium.puppeteer.launch({ args: chromium.args, defaultViewport: { height, width, deviceScaleFactor }, executablePath: await chromium.executablePath, headless: chromium.headless, ignoreHTTPSErrors: true, }) let page = await browser.newPage() await page.goto(`${ baseUrl }/tickets/${ number }?social`) const file = await page.screenshot() await browser.close() return file } finally { if (browser) { await browser.close() } } } 

This module uses chrome-aws-lambda and puppeteer-core to fire up a headless Chrome browser and navigate to a webpage that dynamically builds a page for the attendee's virtual ticket. It then takes a screenshot of this webpage and returns the buffer of bytes.

This is a good time to note that you want to configure the Lambda associated with this event handler to be pretty beefy and not to timeout too quickly. You can accomplish by setting properties in arc.config:

@aws
runtime nodejs14.x
timeout 90
memory 3008
@arc
shared false

The shared false command tells Arc not to build and include code and dependencies from the applications shared folder. This is really important because Lambda has a hard 250MB limit on code/deps and chrome-aws-lambda and puppeteer-core gobble up a ton of that space.

We then save this generated screen to s3:

 const s3 = new AWS.S3() let fileName = `ticket-${ number }.png` await s3 .putObject({ Bucket: process.env.ARC_STATIC_BUCKET, Key : process.env.ARC_STATIC_PREFIX + '/' + fileName, ContentType: 'image/png', Body: file, ACL: 'public-read', }) .promise()

The ARC_STATIC_BUCKET and ARC_STATIC_PREFIX are automatically available in your app's environment variables thanks to Begin.

The last step is to attach this beautiful image to the attendee's custom ticketing page. If you go to my ticketing page and view the source you'll see <meta> tags for Open Graph and Twitter image URLs:

 <meta property="og:image" content="${ socialUrl }" /> <meta name="twitter:image" content="${ socialUrl }"> <meta name="twitter:card" content="summary_large_image">

Phew! I'm sure I skipped a few steps, but you get the gist of how this works and find this helpful! The source code for the CascadiaJS web app can be found on Github at:

https://github.com/cascadiajs/cascadiajs-2021