Why Generate vCard Files Dynamically in CodeIgniter 4?

             In today’s digital world, sharing contact information quickly and reliably is essential for businesses, freelancers, event organizers, CRM systems, and directory websites. Instead of manually creating .vcf files or asking users to fill out forms every time, dynamically generating vCard files directly from your database offers a seamless, professional experience.

With CodeIgniter 4, you can build a powerful contact management system where users (or admins) add contacts once, and visitors can instantly download properly formatted vCard (.vcf) files with a single click. This eliminates typos, saves time, improves mobile compatibility (most phones import vCards natively), and boosts user satisfaction. Search engines also favor pages that solve real user problems — tutorials showing “how to generate vCard in CodeIgniter 4” or “dynamic vCard download PHP” consistently attract organic traffic from developers and small business owners looking for ready-to-use solutions.

Whether you're building a company directory, professional networking tool, lead capture system, or employee contact page, implementing dynamic vCard generation in CodeIgniter 4 adds real value, increases engagement, and makes your application stand out. In this step-by-step guide, you’ll learn exactly how to create, format, and force-download vCard files from database records — complete with clean code, proper headers, character escaping, and best practices for production use.

Key benefits at a glance:

  • One-click contact export to phones, Outlook, Google Contacts
  • Reduces manual data entry errors
  • Improves UX on mobile and desktop
  • SEO-friendly feature that keeps visitors on your site longer
  • Easy to extend with QR codes, photos, addresses, social links

Ready to add this powerful feature to your CodeIgniter 4 project? Let’s dive into the complete implementation.



How to Dynamically Generate and Download vCard Files in CodeIgniter 4

Table Of Content

1 Prerequisites

1.) PHP ≥ 8.1 (CodeIgniter 4 requirement)
2.) Composer
3.) Mysql

2 Introduction

In this tutorial, you'll learn how to dynamically generate vCard (.vcf) files in CodeIgniter 4 and allow users to download contact information directly from your database. This is useful for contact management apps, directories, or CRM-like systems.

We'll build a simple contact list with DataTables, add new contacts via popup, and provide a "Download vCard" button per record.

3 Create / Install a Codeigniter 4 Project

3.1 Install Codeigniter 4 Project

First, make sure your computer has a composer.
Use the following command to install new Codeigniter Project.

composer create-project codeigniter4/appstarter ci-4-vcard-app

Then, navigate to your project directory:

cd ci-4-vcard-app

3.2 Configure Environment and MySql Database

Rename the env file to .env and set the development mode in the .env file also configure mysql:

# CI_ENVIRONMENT = production
CI_ENVIRONMENT = development


DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=ci4_vcard
DB_USERNAME=root
DB_PASSWORD=

Create the database ci4_vcard

4 Create Migration and Model

Create Model for the contacts table:

php spark make:model Contact

Edit app/Models/Contact.php to configure the model:

<?php
namespace App\Models;

use CodeIgniter\Model;

class Contact extends Model
{
    protected $table = 'contacts';
    protected $primaryKey = 'id';
    protected $allowedFields = ['name','email','phone','created_at','updated_at'];
}

Create a migration file for the contacts table:

php spark make:migration CreateContactsTable

Edit the migration file to define the table structure:
    
<?php
namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class CreateContactsTable extends Migration
{
    public function up()
    {
        $this->forge->addField([
            'id' => [
                'type' => 'BIGINT',
                'constraint' => 255,
                'unsigned' => true,
                'auto_increment' => true
            ],
            'name' => [
                'type' => 'VARCHAR',
                'constraint' => '255',
            ],
            'email' => [
                'type' => 'longtext'
            ],
            'phone' => [
                'type' => 'longtext'
            ],
            'created_at' => [
                'type' => 'TIMESTAMP',
                'null' => true
            ],
            'updated_at' => [
                'type' => 'TIMESTAMP',
                'null' => true
            ],
        ]);
        $this->forge->addPrimaryKey('id');
        $this->forge->createTable('contacts');
    }

    public function down()
    {
        $this->forge->dropTable('contacts');
    }
}

Run the migration:

php spark migrate

5 Create ContactController

Create a new controller named ContactController for handling the process of generating vCards:

php spark make:controller ContactController

Controller Function to Export and Download vCard in CodeIgniter:

<?php
namespace App\Controllers;

use App\Controllers\BaseController;
use CodeIgniter\HTTP\ResponseInterface;
use App\Models\Contact;

class ContactController extends BaseController
{
    public function index()
    {
        return view('contacts');
    }
    public function store()
    {
        $jsonStr = file_get_contents('php://input'); 
        $jsonObj = json_decode($jsonStr); 
        $contact_data = $jsonObj->contact_data; 
        $model = new Contact();
        $data = [
            "name" => $contact_data[0],
            "email" => $contact_data[1],
            "phone" => $contact_data[2],
            "created_at"=>date('Y-m-d H:i:s')
        ];
        $model->insert($data);
       $json_data['status']=1;
        return $this->response->setJSON($json_data);
    }
    public function getUsers()
{
    $model = new Contact();
    $request = \Config\Services::request();

     $limit = $request->getPost('length');
    $start = $request->getPost('start');
    $order_column_value = $request->getPost('order')[0]['column'];
    $order=$request->getPost('columns')[$order_column_value]['data'];
    $dir = $request->getPost('order')[0]['dir'];

    $totalData = $model->countAll();
    $totalFiltered = $totalData;

    if(empty($request->getPost('search')['value']))
    {
      $users = $model->orderBy($order, $dir)->findAll($limit, $start);
    }
    else {
        $search = $request->getPost('search')['value'];
        $users = $model->like('name', $search)->orLike('email', $search)->orderBy($order, $dir)->findAll($limit, $start);
       $totalFiltered = $model->like('name', $search)->orLike('email', $search)->countAllResults();
    }
    $data = array();
    if(!empty($users))
    {
        foreach ($users as $user)
        {
            $nestedData['id'] = $user['id'];
            $nestedData['name'] = $user['name'];
            $nestedData['email'] = $user['email'];
            $nestedData['phone'] = $user['phone'];
            $nestedData['created_at'] = $user['created_at'];
            $nestedData['action'] = '<a class="btn btn-success" href="<?= base_url('contact/exportvcard/' . $user['id']) ?>" target="_blank">Download VCard</a>';
            $data[] = $nestedData;
        }
    }

    $json_data = array(
        "draw" => intval($request->getPost('draw')),
        "recordsTotal" => intval($totalData),
        "recordsFiltered" => intval($totalFiltered),
        "data" => $data
    );

    return $this->response->setJSON($json_data);
}
    public function exportvcard($id = null)
    {
        $model = new Contact();
        $contact = $model->find($id);

        if (!$contact) {
            return $this->response->setStatusCode(404)->setBody('Contact not found');
        }

        // Build vCard string (v3.0 standard, with proper escaping)
        $vcf = "BEGIN:VCARD\r\n";
        $vcf .= "VERSION:3.0\r\n";
        $vcf .= "FN:" . $this->escapeVcard($contact['name']) . "\r\n";
        $vcf .= "N:" . $this->escapeVcard($contact['name']) . ";;;\r\n"; // Basic N field
        $vcf .= "EMAIL;TYPE=WORK:" . $this->escapeVcard($contact['email']) . "\r\n";
        $vcf .= "TEL;TYPE=CELL,VOICE:" . $this->escapeVcard($contact['phone']) . "\r\n";
        $vcf .= "REV:" . date('Ymd\THis\Z') . "\r\n";
        $vcf .= "END:VCARD\r\n";

        $filename = str_replace(' ', '_', $contact['name']) . '.vcf';

        return $this->response
            ->setHeader('Content-Type', 'text/vcard; charset=utf-8')
            ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
            ->setBody($vcf);
    }

    private function escapeVcard($string)
    {
        // Basic escaping for vCard (add more for commas, semicolons, etc. as needed)
        return str_replace(["\r", "\n", ",", ";"], ['\\r', '\\n', '\\,', '\\;'], $string);
    }

}
?>

This demonstrates how to generate vCard files in CodeIgniter and facilitates downloading them.

6 Create View (contacts.php)

Create contacts.php in app/Views for listing contacts and enabling vCard generation.

  <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> CodeIgniter 4 vCard Generator</title>
     <link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.0.1/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.datatables.net/1.11.4/css/dataTables.bootstrap5.min.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.5.1.js"></script>
    <script src="https://cdn.datatables.net/1.10.22/js/jquery.dataTables.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.22/js/dataTables.bootstrap5.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>

    <style>
        * {
            margin: 0;
            padding: 0;
            font-family: 'Poppins', sans-serif;
        }

        body {
            background-color: #eee !important;
            background-repeat: no-repeat;
            background-attachment: fixed;
            background-size: cover;
        }

        .main {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
        }

        .container {
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 40px 40px 30px 40px;
            background-color: rgba(255, 255, 255, 0.9);
            border-radius: 15px;
            box-shadow: rgba(0, 0, 0, 0.3) 0 5px 15px;
            width: 60%;
            height: 700px;
            position: absolute;
        }

        .header-container {
            display: flex;
            justify-content: space-between;
            width: 100%;
            border-bottom: 1px solid;
            margin-bottom: 20px;
            padding-bottom: 10px;
        }

        .header-container > h3 {
            font-weight: 500;
        }

        .tasks-container {
            position: relative;
            width: 100%;
        }

        .action-button {
            display: flex;
            justify-content: center;
        }
        
        .action-button > button {
            width: 25px;
            height: 25px;
            font-size: 17px;
            display: flex !important;
            justify-content: center;
            align-items: center;
            margin: 0px 2px;
        }

        .dataTables_wrapper .dataTables_info {
            position: absolute !important;
            bottom: 20px !important;
        }
    </style>
</head>
<body>
    <div class="main">
    <div class="container">
    <div class="header-container">
                <h3>Contact Manager with Export to VCF</h3>
                <button type="button" class="btn btn-dark btn-sm" onClick="addContact()">
                    Add Contact
                </button>
    </div>
    <div class="tasks-container">
        <table id="userTable" class="table table-bordered ">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Name</th>
                    <th>Email</th>
                    <th>Phone</th>
                    <th>Created At</th>
                    <th>Action</th>
                </tr>
            </thead>
            <tbody>
                <!-- Data will be loaded here -->
            </tbody>
        </table>
        </div>
    </div>
    </div>

    <script>
        $(document).ready(function() {
            loadData();
        });
        function loadData()
        {
            $('#userTable').DataTable({
                "processing": true,
                "serverSide": true,
                "ajax": {
                    "url": "<?php echo base_url('contact/getUsers''); ?>",
                    "type": "POST"
                },
                "columns": [
                    { data: 'id' },
                    { data: 'name' },
                    { data: 'email' },
                    { data: 'phone' },
                    { data: 'created_at' },
                    { data: 'action' }
                ]
            });
        }
        function addContact()
        {

            (async () => {
      const { value: formValues } = await Swal.fire({
        title: 'Add Contact',
        confirmButtonText: 'Submit',
        showCloseButton: true,
		    showCancelButton: true,
        html:
          '<input id="name" class="swal2-input" placeholder="Enter Name">' +
          '<input id="email" class="swal2-input" placeholder="Enter Email Id">' +
          '<input id="phone" class="swal2-input" placeholder="Enter Phone No">',
        focusConfirm: false,
        preConfirm: () => {
          return [
            document.getElementById('name').value,
            document.getElementById('email').value,
            document.getElementById('phone').value
          ]
        }
      });

      if (formValues) {
        // Add event
        fetch("/store", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({  contact_data: formValues}),
        })
        .then(response => response.json())
        .then(data => {
          if (data.status == 1) {
            Swal.fire('Contact added successfully!', '', 'success');
            loadData();
          } else {
            Swal.fire(data.error, '', 'error');
          }

          // Refetch events from all sources and rerender
          calendar.refetchEvents();
        })
        .catch(console.error);
      }
    })()

            
        }
        
    </script>
</body>
</html>


7 Define a Route

In app/Config/Routes.php, define routes for the FullCalenderController controller:


use CodeIgniter\Router\RouteCollection;

/**
 * @var RouteCollection $routes
 */
$routes->get('/', 'Home::index');
$routes->get('contacts', 'ContactController::index');
$routes->post('contact/store', 'ContactController::store');
$routes->post('contact/getUsers', 'ContactController::getUsers');
$routes->get('contact/exportvcard/(:num)', 'ContactController::exportvcard/$1');

8 Folder Structure

9 Run and Test

Use the following command to Test the App.

php spark serve

Visit http://localhost:8080/contacts — add contacts, then download vCards.

10 Conclusion

You've now implemented dynamic vCard generation in CodeIgniter 4! For advanced use (photos, addresses, QR codes), consider Composer packages like jeroendesloovere/vcard. This approach ensures proper downloads and compatibility across devices.
Revathi M - PHP and CodeIgniter Developer

Written by Revathi M

PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel

Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.

Frequently Asked Questions

A vCard (.vcf file) is a standard format for electronic business cards that allows users to easily save contact details like name, phone, email, and address to their devices. In web apps, dynamically generating vCards enables instant downloads without sto

In CodeIgniter 4, create a controller method that builds the vCard string using proper headers (Content-Type: text/vcard, Content-Disposition: attachment). Concatenate fields like BEGIN:VCARD, VERSION:3.0 or 4.0, N;, FN;, TEL;, EMAIL;, and END:VCARD, then

vCard 3.0 is widely supported across devices. vCard 4.0 adds more features but may have compatibility issues on older systems. For most applications, start with 3.0.

No, you can build it manually with string concatenation. However, for complex vCards (e.g., photos, multiple phones), consider Composer libraries like jeroendesloovere/vcard.

Set response headers in the controller: Content-Type: text/vcard; charset=utf-8 and Content-Disposition: attachment; filename=\"contact.vcf\". This triggers a download prompt.

Yes, in vCard 3.0 use PHOTO;ENCODING=BASE64;TYPE=JPEG: followed by base64-encoded image data. In 4.0, use PHOTO;VALUE=URI: for a URL or embedded base64.

Yes, properly formatted vCards (especially version 3.0) work well on both iOS and Android, allowing users to add contacts directly.