Front-End Web & Mobile

New in AWS Amplify: Integrate with SQL databases, OIDC/SAML providers, and the AWS CDK

Welcome to “Extensibility Day” of AWS Amplify’s Launch Week! With Amplify Gen 2, we’re providing four new capabilities to customize your app according to your business need. Today, we’re announcing the ability to:

  1. Integrate with existing data sources (including PostgreSQL or MySQL databases)
  2. Authenticate with any OpenID Connect (OIDC) or SAML authentication provider
  3. Customize any of the Amplify-generated AWS service resources (such as Amazon Simple Storage Service (S3) buckets or Amazon DynamoDB tables)
  4. Add any of the 200+ AWS services to your app

Integrate with existing data sources, including PostgreSQL databases

We now offer a first-class integration to connect to existing PostgreSQL or MySQL databases. This gives you real-time subscriptions and fine-grained authorization for PostgreSQL and MySQL databases. The databases can be deployed outside of AWS (for example Neon) or within AWS (for example, Amazon RDS or Amazon Aurora). For our demo today, we’ll be building a real-time API for a PostgreSQL database to allow users to create and subscribe to notes.

Demo of the real-time notes

Not using PostgreSQL or MySQL? No problem. We offer guides on how to integrate with: Amazon DynamoDB, Amazon OpenSearch, Amazon EventBridge, Amazon Polly, Amazon Bedrock, Amazon Rekognition, Amazon Translate, or any HTTP endpoint. Not seeing your data source listed here? You can always use our functions capability to integrate with anything that can be connected to a AWS Lambda function. You get the picture – Amplify Gen 2 integrates with anything.

Let’s walk through an integration with Neon, a non-AWS PostgreSQL database service. First, let’s setup a starter React application with Amplify Gen 2:

npm create vite@latest gen2-postgres-test

Pick “React + TypeScript” as the starter option. Then initialize a new Amplify Gen 2 project:

npm create amplify@latest

In addition, also install our React UI library to quickly set up an authentication UI that we’ll use later:

npm install @aws-amplify/ui-react

Next, go to Neon’s website and create a new database. You can use the following PostgreSQL schema:

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS postgis;

-- Create a table to events
CREATE TABLE events (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100),
  geom GEOMETRY(Point, 4326)
);

-- Create a spatial index on the geometry column
CREATE INDEX idx_events_geom ON events USING GIST (geom);

-- Insert some sample data
INSERT INTO events (name, geom)
VALUES
  ('Event A', ST_SetSRID(ST_MakePoint(-73.985428, 40.748817), 4326)),
  ('Event B', ST_SetSRID(ST_MakePoint(-73.990673, 40.742051), 4326)),
  ('Event C', ST_SetSRID(ST_MakePoint(-73.981226, 40.744681), 4326)),
  ('Event D', ST_SetSRID(ST_MakePoint(-73.987369, 40.739917), 4326)),
  ('Event E', ST_SetSRID(ST_MakePoint(-73.992743, 40.746035), 4326));

-- Create a notes table where we'll add owner-based authorization
CREATE TABLE notes (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    content TEXT,
    owner TEXT
);

Demo showing how to create a DB schema in Neon

Next, get the database connection string and provide it to Amplify as a secret:

npx ampx sandbox secret set SQL_CONNECTION_STRING

Then generate a TypeScript representation of your SQL database schema:

npx ampx generate schema-from-database --connection-uri-secret SQL_CONNECTION_STRING --out amplify/data/schema.sql.ts

Demo of how to set SQL connection string

You can then import your schema into your amplify/data/resource.ts file, apply authorization rules on tables to allow create, read, update, or delete operations to your SQL databases via Amplify’s Data client.

In the example below, we’re adding an owner authorization renaming the notes table to Note so it’s better represented in the Data client.

import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
import { schema as generatedSqlSchema} from './schema.sql'

const schema = generatedSqlSchema.renameModels(() => [
  ['notes', 'Note'] // Renames the models from "notes" -> "Note"
]).setAuthorization((models) => [
  // Defines owner-based authorization rule and isolates the record
  // based on the record owner. The owner is the user identifier 
  // stored in the column named "owner".
  models.Note.authorization(allow => allow.ownerDefinedIn('owner'))
])

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
  },
});

Start the cloud sandbox to start iterating on your application data backend.

npx ampx sandbox

Next, on the client side, you can use the fully-typed Data client to retrieve or update your backend data. For example, in your React code, you can re-use Amplify’s real-time subscriptions capability to observeQuery incoming notes. Edit the src/App.tsx file with the following code:

import { useEffect, useState } from 'react'
import { generateClient } from 'aws-amplify/api'
import { type Schema } from '../amplify/data/resource'
import { Amplify } from 'aws-amplify'
import outputs from '../amplify_outputs.json'
import { withAuthenticator } from '@aws-amplify/ui-react'
import '@aws-amplify/ui-react/styles.css'

Amplify.configure(outputs)

const client = generateClient<Schema>();

function App() {
  const [notes, setNotes] = useState<Schema["Note"]["type"][]>([])
  useEffect(() => {
    const sub = client.models.Note.observeQuery().subscribe({
      next: (data) => {
        setNotes([...data.items])
      }
    })
    return () => sub.unsubscribe() 
  }, [])

  return (
    <>
      <h1>Note</h1>
      <ul>
        {notes?.map(note => (
          <li key={note.id} onClick={async () => {
            await client.models.Note.delete({ id: note.id })
          }}>
            <div>{note.content}</div>
          </li>
        ))}
      </ul>
      <button onClick={async () => {
        await client.models.Note.create({
          content: window.prompt("New note?"),
        })
      }}>Create Note</button>
    </>
  )
}

export default withAuthenticator(App)

Test your changes locally and you should be able to create notes in real-time.

npm run dev

In the demo video below, I’m logged in as the same user between these windows and the newly created notes in one window are automatically synced to the other window:

Demo of the real-time notes

You can also add custom queries or mutations that execute specific SQL queries. For example, you can use PostgreSQL extensions, such as PostGIS for geospatial queries:

const schema = generatedSqlSchema.renameModels(() => [
  ['notes', 'Note']
]).setAuthorization((models) => [
  models.Note.authorization(allow => allow.ownerDefinedIn('owner'))
]).addToSchema({
  // Adds a new query that uses PostgreSQL's PostGIS extension to
  // convert a geom type to lat and long coordinates
  listEventWithCoord: a.query()
    .returns(a.ref('EventWithCoord').array())
    .authorization(allow => allow.authenticated())
    .handler(a.handler.inlineSql(`
      SELECT id, name, ST_X(geom) AS longitude, ST_Y(geom) AS latitude FROM events;
    `)),
  
  // Defines a custom type that matches the response shape of the query
  EventWithCoord: a.customType({
    id: a.integer(),
    name: a.string(),
    latitude: a.float(),
    longitude: a.float(),
  })
})

You can call this new query via the Data client. For example, let’s add a button and create a pop-up alert of the result:

<button onClick={async () => {
    const { data } = await client.queries.listEventWithCoord()
    window.alert(JSON.stringify(data))
}}>Fetch events</button>

Demo of fetching events

Once you’re ready, deploy the new API backed by your PostgreSQL database by committing to Git and hosting it with Amplify. Make sure to set the SQL_CONNECTION_STRING secret within the AWS Amplify console for your branch environment.

Authenticate with any OpenID Connect or SAML authentication provider

Amplify Gen 2 provides the flexibility to authenticate users with any OpenID Connect (OIDC) provider or any SAML provider. This means you can integrate your app with popular identity providers like Microsoft EntraID, Auth0, Okta, or even your own custom OIDC or SAML provider.

To setup a OIDC provider, you can configure them in amplify/auth/resource.ts file on the defineAuth handler. For example, if you would like to setup a Microsoft EntraID provider, you can do so as follows:

import { defineAuth, secret } from '@aws-amplify/backend';

export const auth = defineAuth({
  loginWith: {
    email: true,
    externalProviders: {
      oidc: [
        {
          name: 'MicrosoftEntraID',
          clientId: secret('MICROSOFT_ENTRA_ID_CLIENT_ID'),
          clientSecret: secret('MICROSOFT_ENTRA_ID_CLIENT_SECRET'),
          issuerUrl: '<your-issuer-url>',
        },
      ],
      logoutUrls: ['http://localhost:3000/', 'https://mywebsite.com'],
      callbackUrls: [
        'http://localhost:3000/profile',
        'https://mywebsite.com/profile',
      ],
    },
  },
});

Note: you must set the secrets either in the AWS console for your production branches or via npx ampx sandbox secret set for your cloud sandbox.

On the frontend, simply call the signInWithRedirect() function to trigger the OIDC/SAML authentication workflow:

import { signInWithRedirect } from 'aws-amplify/auth';

await signInWithRedirect({
  provider: {
    custom: 'MicrosoftEntraID'
  }
});

Customize any of the Amplify-generated AWS service resources

Customers have told us that “using Amplify feels like using a cheat code to AWS”. However, there will always come a time when you need to fine-tune a generated resource to optimize your use case. We like to think of this as: magic; not mystery.

“What if I need to delete files stored in my bucket after 30 days?”

Without Amplify, you’d need to build a complex cron job that triggers a function every 30 days and then traverse through all the files and delete them. That’s one more capability to maintain, one more capability to take care of, and one more capability to pay for.

With Amplify, you can leverage the full capability set of the underlying service. Amplify Storage is built with Amazon S3, which allows you to configure object lifecycle rules. All you have to set an option on the bucket. In your amplify/backend.ts file, just set that optimization on the generated S3 bucket.

const backend = defineBackend({
  auth,
  data,
  storage,
});

// Override generated resources
backend.storage.resources.cfnResources.cfnBucket.lifecycleConfiguration = {
  rules: [{
    expirationInDays: 30,
    prefix: 'room/',
    status: 'Enabled'
  }]
}

We support the ability to override for all our Amplify’s capabilities: data, auth, storage, and functions.

Add any of the 200+ AWS services to your app

As you grow, you will inevitably need to expand your capabilities beyond what Amplify provides built-in — and that’s okay. Every app should leverage the best service to accomplish its job. That’s why we ensured that you can add any of the 200+ AWS services to your Amplify app.

Amplify Gen 2 is built on top of AWS Cloud Development Kit (CDK), which allows you to develop reliable, scalable, cost-effective applications in the cloud with the considerable expressive power of TypeScript.

“What if I need to add Amazon Bedrock for generative AI use cases to my application or a notification fan-out capability?”

Without Amplify, you’d need to manage an entirely separate backend infrastructure codebase from your application frontend. This means spending weeks of DevOps overhead to synchronize deployments between your app frontend and backend and building custom dev, test, prod workflows.

With Amplify, you can just add any additional backend infrastructure to your Amplify app. You get all of Amplify’s CI/CD capabilities built-in. It’s DevOps that – just works. Need a new environment? Git branch. Need a sandbox environment for testing quickly with your local frontend code? Use Amplify’s cloud sandbox.

To add any resource from the 200+ AWS services, import their respective CDK constructs and add them to a stack in your amplify/backend.ts file. You can of course – because Amplify is built by and on AWS – connect these custom resources with Amplify-generated resources.

import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';
import { generateHaiku } from './functions/generateHaiku/resource';
import { handleSubscription } from './functions/handleSubscription/resource';
// Import CDK constructs directly into an Amplify Project
import * as iam from 'aws-cdk-lib/aws-iam';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';

const backend = defineBackend({
  auth,
  data,
  generateHaiku,
  handleSubscription
});

// Grant function access to Amazon Bedrock
backend.generateHaiku.resources.lambda.addToRolePolicy(
  new iam.PolicyStatement({
    effect: Effect.ALLOW,
    actions: ["bedrock:InvokeModel"],
    resources: [
      `arn:aws:bedrock:${Stack.of(backend.data).region}::foundation-model/anthropic.claude-3-haiku-20240307-v1:0`,
    ],
  })
)

// Define a new stack for the SNS topic
const snsStack = backend.createStack("SnsStack");

// Create the SNS topic
const topic = new sns.Topic(snsStack, "Topic");

// Use an Amplify Function (defineFunction) as a subscription handler 
topic.addSubscription(new subscriptions.LambdaSubscription(
  backend.handleSubscription.resources.lambda
));

Conclusion

This is just a small glimpse of how you can extend your AWS Amplify project. We want to eliminate the days wasted on undifferentiated hand-authored code for DevOps workflows or datasource connectivity. Instead of worrying about infrastructure configuration, we want you to focus on your application use cases. To get started, try one of Amplify’s Quickstart tutorials!