diff --git a/Cargo.lock b/Cargo.lock
index 239680d41ee8ca..339fc0ab98776e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8877,11 +8877,14 @@ name = "turbopack-ecmascript-plugins"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "async-trait",
  "serde",
  "swc_core",
  "swc_emotion",
+ "swc_relay",
  "turbo-tasks",
  "turbo-tasks-build",
+ "turbo-tasks-fs",
  "turbopack-ecmascript",
 ]
 
diff --git a/crates/turbopack-ecmascript-plugins/Cargo.toml b/crates/turbopack-ecmascript-plugins/Cargo.toml
index 399d9bae5d4cb7..b9552a5796813a 100644
--- a/crates/turbopack-ecmascript-plugins/Cargo.toml
+++ b/crates/turbopack-ecmascript-plugins/Cargo.toml
@@ -14,13 +14,16 @@ transform_emotion = []
 
 [dependencies]
 anyhow = { workspace = true }
+async-trait = { workspace = true }
 serde = { workspace = true }
 
 turbo-tasks = { workspace = true }
+turbo-tasks-fs = { workspace = true }
 turbopack-ecmascript = { workspace = true }
 
 swc_core = { workspace = true, features = ["ecma_ast", "ecma_visit", "common"] }
 swc_emotion = { workspace = true }
+swc_relay = { workspace = true }
 
 [build-dependencies]
 turbo-tasks-build = { workspace = true }
diff --git a/crates/turbopack-ecmascript-plugins/src/transform/emotion.rs b/crates/turbopack-ecmascript-plugins/src/transform/emotion.rs
index 63c1eb184759a1..be0e0b8038f225 100644
--- a/crates/turbopack-ecmascript-plugins/src/transform/emotion.rs
+++ b/crates/turbopack-ecmascript-plugins/src/transform/emotion.rs
@@ -5,6 +5,7 @@ use std::{
 };
 
 use anyhow::Result;
+use async_trait::async_trait;
 use serde::{Deserialize, Serialize};
 use swc_core::{
     common::util::take::Take,
@@ -91,8 +92,13 @@ impl EmotionTransformer {
     }
 }
 
+#[async_trait]
 impl CustomTransformer for EmotionTransformer {
-    fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Option<Program> {
+    async fn transform(
+        &self,
+        program: &mut Program,
+        ctx: &TransformContext<'_>,
+    ) -> Result<Option<Program>> {
         #[cfg(feature = "transform_emotion")]
         {
             let p = std::mem::replace(program, Program::Module(Module::dummy()));
@@ -111,7 +117,7 @@ impl CustomTransformer for EmotionTransformer {
             ));
         }
 
-        None
+        Ok(None)
     }
 }
 
diff --git a/crates/turbopack-ecmascript-plugins/src/transform/mod.rs b/crates/turbopack-ecmascript-plugins/src/transform/mod.rs
index 3697907f45254b..ba33ce94d8bd6c 100644
--- a/crates/turbopack-ecmascript-plugins/src/transform/mod.rs
+++ b/crates/turbopack-ecmascript-plugins/src/transform/mod.rs
@@ -1 +1,2 @@
 pub mod emotion;
+pub mod relay;
diff --git a/crates/turbopack-ecmascript-plugins/src/transform/relay.rs b/crates/turbopack-ecmascript-plugins/src/transform/relay.rs
new file mode 100644
index 00000000000000..4e13c7537f0c26
--- /dev/null
+++ b/crates/turbopack-ecmascript-plugins/src/transform/relay.rs
@@ -0,0 +1,57 @@
+use std::path::PathBuf;
+
+use anyhow::Result;
+use async_trait::async_trait;
+use swc_core::{
+    common::{util::take::Take, FileName},
+    ecma::{
+        ast::{Module, Program},
+        visit::FoldWith,
+    },
+};
+use turbopack_ecmascript::{CustomTransformer, TransformContext};
+
+#[derive(Debug)]
+pub struct RelayTransformer {
+    config: swc_relay::Config,
+}
+
+impl RelayTransformer {
+    pub fn new(config: swc_relay::Config) -> Self {
+        Self { config }
+    }
+}
+
+#[async_trait]
+impl CustomTransformer for RelayTransformer {
+    async fn transform(
+        &self,
+        program: &mut Program,
+        ctx: &TransformContext<'_>,
+    ) -> Result<Option<Program>> {
+        // If user supplied artifact_directory, it should be resolvable already.
+        // Otherwise, supply default relative path (./__generated__)
+        let (root, config) = if self.config.artifact_directory.is_some() {
+            (PathBuf::new(), None)
+        } else {
+            let config = swc_relay::Config {
+                artifact_directory: Some(PathBuf::from("__generated__")),
+                ..self.config
+            };
+            (PathBuf::from("."), Some(config))
+        };
+
+        let p = std::mem::replace(program, Program::Module(Module::dummy()));
+        *program = p.fold_with(&mut swc_relay::relay(
+            config.as_ref().unwrap_or_else(|| &self.config),
+            FileName::Real(PathBuf::from(ctx.file_name_str)),
+            root,
+            // [TODO]: pages_dir comes through next-swc-loader
+            // https://github.com/vercel/next.js/blob/ea472e8058faea8ebdab2ef6d3aab257a1f0d11c/packages/next/src/build/webpack-config.ts#L792
+            None,
+            Some(ctx.unresolved_mark),
+        ));
+
+        Ok(None)
+    }
+}
diff --git a/crates/turbopack-ecmascript/src/lib.rs b/crates/turbopack-ecmascript/src/lib.rs
index ad92d35af8d4b0..a83f07615efb32 100644
--- a/crates/turbopack-ecmascript/src/lib.rs
+++ b/crates/turbopack-ecmascript/src/lib.rs
@@ -42,8 +42,9 @@ use swc_core::{
     },
 };
 pub use transform::{
-    CustomTransformer, EcmascriptInputTransform, EcmascriptInputTransformsVc, TransformContext,
-    TransformPlugin, TransformPluginVc,
+    CustomTransformer, EcmascriptInputTransform, EcmascriptInputTransformsVc,
+    OptionTransformPlugin, OptionTransformPluginVc, TransformContext, TransformPlugin,
+    TransformPluginVc,
 };
 use turbo_tasks::{
     primitives::StringVc, trace::TraceRawVcs, RawVc, ReadRef, TryJoinIterExt, Value, ValueToString,
diff --git a/crates/turbopack-ecmascript/src/transform/mod.rs b/crates/turbopack-ecmascript/src/transform/mod.rs
index f58f51c9500bfc..aca096b2e06756 100644
--- a/crates/turbopack-ecmascript/src/transform/mod.rs
+++ b/crates/turbopack-ecmascript/src/transform/mod.rs
@@ -4,6 +4,7 @@ mod util;
 use std::{fmt::Debug, hash::Hash, path::PathBuf, sync::Arc};
 
 use anyhow::Result;
+use async_trait::async_trait;
 use swc_core::{
     base::SwcComments,
     common::{chain, util::take::Take, FileName, Mark, SourceMap},
@@ -77,8 +78,13 @@ pub enum EcmascriptInputTransform {
 
 /// The CustomTransformer trait allows you to implement your own custom SWC
 /// transformer to run over all ECMAScript files imported in the graph.
+#[async_trait]
 pub trait CustomTransformer: Debug {
-    fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Option<Program>;
+    async fn transform(
+        &self,
+        program: &mut Program,
+        ctx: &TransformContext<'_>,
+    ) -> Result<Option<Program>>;
 }
 
 /// A wrapper around a TransformPlugin instance, allowing it to operate with
@@ -93,9 +99,23 @@ pub trait CustomTransformer: Debug {
 #[derive(Debug)]
 pub struct TransformPlugin(#[turbo_tasks(trace_ignore)] Box<dyn CustomTransformer + Send + Sync>);
 
+#[turbo_tasks::value(transparent)]
+pub struct OptionTransformPlugin(Option<TransformPluginVc>);
+
+impl Default for OptionTransformPluginVc {
+    fn default() -> Self {
+        OptionTransformPluginVc::cell(None)
+    }
+}
+
+#[async_trait]
 impl CustomTransformer for TransformPlugin {
-    fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Option<Program> {
-        self.0.transform(program, ctx)
+    async fn transform(
+        &self,
+        program: &mut Program,
+        ctx: &TransformContext<'_>,
+    ) -> Result<Option<Program>> {
+        self.0.transform(program, ctx).await
     }
 }
 
@@ -323,7 +343,7 @@ impl EcmascriptInputTransform {
                 }
             }
             EcmascriptInputTransform::Plugin(transform) => {
-                if let Some(output) = transform.await?.transform(program, ctx) {
+                if let Some(output) = transform.await?.transform(program, ctx).await? {
                     *program = output;
                 }
             }
diff --git a/crates/turbopack/src/module_options/mod.rs b/crates/turbopack/src/module_options/mod.rs
index f5e6a0afd22899..9876d5659ba6e6 100644
--- a/crates/turbopack/src/module_options/mod.rs
+++ b/crates/turbopack/src/module_options/mod.rs
@@ -79,6 +79,7 @@ impl ModuleOptionsVc {
             preset_env_versions,
             ref custom_ecmascript_app_transforms,
             ref custom_ecmascript_transforms,
+            ref custom_ecma_transform_plugins,
             ref custom_rules,
             execution_context,
             ref rules,
@@ -93,7 +94,30 @@ impl ModuleOptionsVc {
                 }
             }
         }
-        let mut transforms = custom_ecmascript_app_transforms.clone();
+
+        let (before_transform_plugins, after_transform_plugins) =
+            if let Some(transform_plugins) = custom_ecma_transform_plugins {
+                let transform_plugins = transform_plugins.await?;
+                (
+                    transform_plugins
+                        .source_transforms
+                        .iter()
+                        .cloned()
+                        .map(EcmascriptInputTransform::Plugin)
+                        .collect(),
+                    transform_plugins
+                        .output_transforms
+                        .iter()
+                        .cloned()
+                        .map(|plugin| EcmascriptInputTransform::Plugin(plugin))
+                        .collect(),
+                )
+            } else {
+                (vec![], vec![])
+            };
+
+        let mut transforms = before_transform_plugins;
+        transforms.extend(custom_ecmascript_app_transforms.iter().cloned());
         transforms.extend(custom_ecmascript_transforms.iter().cloned());
 
         // Order of transforms is important. e.g. if the React transform occurs before
@@ -182,6 +206,7 @@ impl ModuleOptionsVc {
                     .iter()
                     .cloned()
                     .chain(transforms.iter().cloned())
+                    .chain(after_transform_plugins.iter().cloned())
                     .collect(),
             )
         } else {
@@ -202,6 +227,7 @@ impl ModuleOptionsVc {
             .iter()
             .cloned()
             .chain(transforms.iter().cloned())
+            .chain(after_transform_plugins.iter().cloned())
             .collect(),
         );
 
@@ -222,6 +248,7 @@ impl ModuleOptionsVc {
             .iter()
             .cloned()
             .chain(transforms.iter().cloned())
+            .chain(after_transform_plugins.iter().cloned())
             .collect(),
         );
 
diff --git a/crates/turbopack/src/module_options/module_options_context.rs b/crates/turbopack/src/module_options/module_options_context.rs
index a3451616007d10..b361f5ebace46c 100644
--- a/crates/turbopack/src/module_options/module_options_context.rs
+++ b/crates/turbopack/src/module_options/module_options_context.rs
@@ -2,7 +2,7 @@ use indexmap::IndexMap;
 use serde::{Deserialize, Serialize};
 use turbo_tasks::trace::TraceRawVcs;
 use turbopack_core::{environment::EnvironmentVc, resolve::options::ImportMappingVc};
-use turbopack_ecmascript::EcmascriptInputTransform;
+use turbopack_ecmascript::{EcmascriptInputTransform, TransformPluginVc};
 use turbopack_ecmascript_plugins::transform::emotion::EmotionTransformConfigVc;
 use turbopack_node::{
     execution_context::ExecutionContextVc, transforms::webpack::WebpackLoaderConfigItemsVc,
@@ -156,6 +156,18 @@ impl Default for StyledComponentsTransformConfigVc {
     }
 }
 
+/// Configuration options for the custom ecma transform to be applied.
+#[turbo_tasks::value(shared)]
+#[derive(Default, Clone)]
+pub struct CustomEcmascriptTransformPlugins {
+    /// List of plugins to be applied before the main transform.
+    /// Transform will be applied in the order of the list.
+    pub source_transforms: Vec<TransformPluginVc>,
+    /// List of plugins to be applied after the main transform.
+    /// Transform will be applied in the order of the list.
+    pub output_transforms: Vec<TransformPluginVc>,
+}
+
 #[turbo_tasks::value(shared)]
 #[derive(Default, Clone)]
 pub struct ModuleOptionsContext {
@@ -187,11 +199,15 @@ pub struct ModuleOptionsContext {
     pub enable_mdx_rs: bool,
     #[serde(default)]
     pub preset_env_versions: Option<EnvironmentVc>,
+    #[deprecated(note = "use custom_ecma_transform_plugins instead")]
     #[serde(default)]
     pub custom_ecmascript_app_transforms: Vec<EcmascriptInputTransform>,
+    #[deprecated(note = "use custom_ecma_transform_plugins instead")]
     #[serde(default)]
     pub custom_ecmascript_transforms: Vec<EcmascriptInputTransform>,
     #[serde(default)]
+    pub custom_ecma_transform_plugins: Option<CustomEcmascriptTransformPluginsVc>,
+    #[serde(default)]
     /// Custom rules to be applied after all default rules.
     pub custom_rules: Vec<ModuleRule>,
     #[serde(default)]